像 JS 这样带有复制 GC 的语言是否曾经在 cpu 寄存器中存储过任何东西?
Do languages like JS with a copying GC ever store anything on the cpu registers?
我正在学习 GC,我知道有一个叫做 HandleScope
的东西,它 'protects' 来自 GC 的局部变量,并在发生 gc 堆复制时更新它们。例如,如果我有一个将 2 个值相加的例程并调用它,它可能会调用垃圾收集器,它将复制我的值指向的对象(或者 GC 甚至不知道该值指向的对象被引用)。一个非常简单的例子:
#include <vector>
Value addValues(Value a, Value b);
std::vector<Value*> gc_vals_with_protection;
Value func(Value a, Value b)
{
vars.push_back(&a); // set protection for a
gc_vals_with_protection.push_back(&b); // set protection for b
Value res = addValues(a, b); // do calcuations
gc_vals_with_protection.pop_back(); // remove protection for b
gc_vals_with_protection.pop_back(); // remove protection for a
return res;
}
但这让我开始思考,这意味着 a
和 b
永远不会出现在物理 CPU 寄存器上,因为您已经获取了它们的地址(并且 CPU 寄存器没有地址)这将使对它们的计算效率低下。此外,在每个函数的开头,您必须向后推两次向量(https://godbolt.org/z/dc6vY1Yc5 用于汇编)。
我想我可能遗漏了一些东西,因为这一定不是最优的。我还缺少其他技巧吗?
(此处为 V8 开发人员。)
Do languages like JS with a copying GC ever store anything on the cpu registers?
当然可以。 CPU 所做的几乎所有事情都涉及它的寄存器。
也就是说,JavaScript 对象通常分配在堆上,至少有以下原因:
(1) 它们比寄存器大。所以寄存器通常保存指向堆上对象的指针。需要句柄的是这些指针,而不是对象本身(既要更新它们,又要通知 GC 存在对相关对象的引用,因此不得释放该对象)。
(2) 它们往往 longer-lived 比您可以在寄存器中保存某些内容的典型时间要多 longer-lived ,这只是几条机器指令:由于寄存器集非常小,因此它们被重复用于某些事情否则 一直(不管 JavaScript 或 GC 等),所以他们之前持有的任何东西都会被溢出(通常但不一定到堆栈),或者 re-read 下次需要时,无论它最初来自哪里。
(3) 它们具有“指针身份”:JavaScript 类似 obj1 === obj2
的代码(针对对象,而不是基元)只有在对象存储在一个位置时才能正常工作。尝试将对象存储在寄存器中意味着将它们四处复制,这会破坏它。
创建句柄肯定有一些成本;不过,这比向 std::vector
添加内容要快。
此外,当将句柄从一个函数传递到另一个函数时,被调用函数不必 re-register 任何东西:句柄可以传递,而不必在 HandleScope 的后备存储中创建新条目。
一个非常重要的观察结果是 JavaScript 函数不需要本地句柄。在执行 JavaScript 时,V8 仔细跟踪堆栈的内容(即寄存器溢出的内容),并可以直接遍历和更新堆栈。只有处理 JS 对象的 C++ 代码才需要 HandleScopes,因为这种技术对于 C++ 堆栈帧(由 C++ 编译器控制)是不可能的。此类 C++ 代码通常不是应用程序最关键的性能瓶颈;因此,虽然它的性能肯定 很重要 ,但一些开销是可以接受的。
(旁注:可以“盲目地”(即不了解其内容)扫描 C++ 堆栈帧并执行 so-called“保守”(而不是“精确”)垃圾收集;这有其优点和缺点,特别是它使移动 GC 变得不可能,因此与您的问题没有直接关系。)
更进一步:足够“热”的函数将被编译为优化的机器代码;这段代码是仔细分析的结果,因此可以非常积极地在寄存器中尽可能长时间地保留 values(主要是数字),例如在最终结果最终出现之前的计算链存储在某些对象的某些 属性 中。
为了完整起见,我还要提到有时,整个对象可以保存在寄存器中:这是优化编译器成功执行“逃逸分析”并可以证明该对象永远不会“逃脱”到外界。一个简单的例子是:
function silly(a, b) {
let vector = {x: a, y: b};
return vector.x + vector.y;
}
优化此函数后,编译器可以证明 vector
永远不会逃逸,因此它可以跳过分配并将 a
和 b
保留在寄存器中(或至少作为“独立值”,如果函数更大并且需要这些寄存器用于其他用途,它们可能仍会溢出到堆栈中。
我正在学习 GC,我知道有一个叫做 HandleScope
的东西,它 'protects' 来自 GC 的局部变量,并在发生 gc 堆复制时更新它们。例如,如果我有一个将 2 个值相加的例程并调用它,它可能会调用垃圾收集器,它将复制我的值指向的对象(或者 GC 甚至不知道该值指向的对象被引用)。一个非常简单的例子:
#include <vector>
Value addValues(Value a, Value b);
std::vector<Value*> gc_vals_with_protection;
Value func(Value a, Value b)
{
vars.push_back(&a); // set protection for a
gc_vals_with_protection.push_back(&b); // set protection for b
Value res = addValues(a, b); // do calcuations
gc_vals_with_protection.pop_back(); // remove protection for b
gc_vals_with_protection.pop_back(); // remove protection for a
return res;
}
但这让我开始思考,这意味着 a
和 b
永远不会出现在物理 CPU 寄存器上,因为您已经获取了它们的地址(并且 CPU 寄存器没有地址)这将使对它们的计算效率低下。此外,在每个函数的开头,您必须向后推两次向量(https://godbolt.org/z/dc6vY1Yc5 用于汇编)。
我想我可能遗漏了一些东西,因为这一定不是最优的。我还缺少其他技巧吗?
(此处为 V8 开发人员。)
Do languages like JS with a copying GC ever store anything on the cpu registers?
当然可以。 CPU 所做的几乎所有事情都涉及它的寄存器。
也就是说,JavaScript 对象通常分配在堆上,至少有以下原因:
(1) 它们比寄存器大。所以寄存器通常保存指向堆上对象的指针。需要句柄的是这些指针,而不是对象本身(既要更新它们,又要通知 GC 存在对相关对象的引用,因此不得释放该对象)。
(2) 它们往往 longer-lived 比您可以在寄存器中保存某些内容的典型时间要多 longer-lived ,这只是几条机器指令:由于寄存器集非常小,因此它们被重复用于某些事情否则 一直(不管 JavaScript 或 GC 等),所以他们之前持有的任何东西都会被溢出(通常但不一定到堆栈),或者 re-read 下次需要时,无论它最初来自哪里。
(3) 它们具有“指针身份”:JavaScript 类似 obj1 === obj2
的代码(针对对象,而不是基元)只有在对象存储在一个位置时才能正常工作。尝试将对象存储在寄存器中意味着将它们四处复制,这会破坏它。
创建句柄肯定有一些成本;不过,这比向 std::vector
添加内容要快。
此外,当将句柄从一个函数传递到另一个函数时,被调用函数不必 re-register 任何东西:句柄可以传递,而不必在 HandleScope 的后备存储中创建新条目。
一个非常重要的观察结果是 JavaScript 函数不需要本地句柄。在执行 JavaScript 时,V8 仔细跟踪堆栈的内容(即寄存器溢出的内容),并可以直接遍历和更新堆栈。只有处理 JS 对象的 C++ 代码才需要 HandleScopes,因为这种技术对于 C++ 堆栈帧(由 C++ 编译器控制)是不可能的。此类 C++ 代码通常不是应用程序最关键的性能瓶颈;因此,虽然它的性能肯定 很重要 ,但一些开销是可以接受的。
(旁注:可以“盲目地”(即不了解其内容)扫描 C++ 堆栈帧并执行 so-called“保守”(而不是“精确”)垃圾收集;这有其优点和缺点,特别是它使移动 GC 变得不可能,因此与您的问题没有直接关系。)
更进一步:足够“热”的函数将被编译为优化的机器代码;这段代码是仔细分析的结果,因此可以非常积极地在寄存器中尽可能长时间地保留 values(主要是数字),例如在最终结果最终出现之前的计算链存储在某些对象的某些 属性 中。
为了完整起见,我还要提到有时,整个对象可以保存在寄存器中:这是优化编译器成功执行“逃逸分析”并可以证明该对象永远不会“逃脱”到外界。一个简单的例子是:
function silly(a, b) {
let vector = {x: a, y: b};
return vector.x + vector.y;
}
优化此函数后,编译器可以证明 vector
永远不会逃逸,因此它可以跳过分配并将 a
和 b
保留在寄存器中(或至少作为“独立值”,如果函数更大并且需要这些寄存器用于其他用途,它们可能仍会溢出到堆栈中。