最近,我遇到了V8使用的隐藏类和内联缓存的概念,以优化JS代码。酷。
我知道对象在内部表示为隐藏类。两个对象可能具有相同的属性,但隐藏的类别不同(取决于分配属性的顺序(。
v8也使用内联缓存概念直接检查偏移以访问对象的属性,而不是使用对象的隐藏类确定偏移。
代码 -
function Point(x, y) {
this.x = x;
this.y = y;
}
function processPoint(point) {
// console.log(point.x, point.y, point.a, point.b);
// let x = point;
}
function main() {
let p1 = new Point(1, 1);
let p2 = new Point(1, 1);
let p3 = new Point(1, 1);
const N = 300000000;
p1.a = 1;
p1.b = 1;
p2.b = 1;
p2.a = 1;
p3.a = 1;
p3.b = 1;
let start_1 = new Date();
for(let i = 0; i< N; i++ ) {
if (i%4 != 0) {
processPoint(p1);
} else {
processPoint(p2)
}
}
let end_1 = new Date();
let t1 = (end_1 - start_1);
let start_2 = new Date();
for(let i = 0; i< N; i++ ) {
if (i%4 != 0) {
processPoint(p1);
} else {
processPoint(p1)
}
}
let end_2 = new Date();
let t2 = (end_2 - start_2);
let start_3 = new Date();
for(let i = 0; i< N; i++ ) {
if (i%4 != 0) {
processPoint(p1);
} else {
processPoint(p3)
}
}
let end_3 = new Date();
let t3 = (end_3 - start_3);
console.log(t1, t2, t3);
}
(function(){
main();
})();
我期望结果像 t1>(t2 = t3(因为:
第一个循环:V8将尝试在运行两次后尝试优化,但它很快会遇到不同的隐藏类,因此它将进行优化。
第二个循环:始终调用相同的对象,因此可以使用内联缓存。
第三回路:与第二个循环相同,因为隐藏类是相同的。
但结果不令人满意。我得到(类似的结果一次又一次运行( -
3553 4805 4556
问题:
为什么结果不如预期?我的假设在哪里出错?
我如何更改此代码以演示隐藏的类和内联缓存性能改进?
我是否从开始中弄错了?
通过让对象共享它们是为了记忆效率而存在的隐藏类?
还有其他一些简单的绩效改进示例?
我正在使用节点8.9.4进行测试。预先感谢。
来源:
https://blog.sessionstack.com/how-javascript-works-inside-the--v8-g8-g8-g8-pips-5-tips-on-how-write-write-write-optimatire-code-ac089e6e62b12e
https://draft.li/blog/2016/12/22/javascript-engines-engines-hiddend-classes/
https://richardartoul.github.io/jekyll/jekyll/update/2015/04/26/hiddend-classes.html
以及更多..
v8开发人员在这里。摘要是: Microbenchmarking很难,不要这样做。
首先,使用您的代码发布的代码,我将380 380 380
视为输出,这是可以预期的,因为function processPoint
是空的,因此所有循环都执行相同的工作(即无效(,无论您选择哪个点对象。
测量单态和2向多态性内联粘贴之间的性能差异很困难,因为它不是很大,因此您必须非常谨慎地对基准还做什么。例如,console.log
是如此慢,以至于它会遮蔽其他所有内容。
您还必须谨慎对待内联的影响。当您的基准测试有许多迭代时,代码将得到优化(运行WAAAAY两次以上(,并且优化编译器(在某种程度上(内联函数将允许后续优化(具体来说:消除各种事情(,从而可以大大进行。更改您的测量内容。编写有意义的微型基准很难;您将不会检查生成的组装和/或了解您正在调查的JavaScript引擎的实现细节。
要记住的另一件事是内联缓存的位置,以及随着时间的流逝,它们将拥有什么状态。忽略内衬,像processPoint
这样的函数不知道或关心它的何处。一旦其内联缓存是多态性的,即使以后在您的基准测试中(在这种情况下,在第二个和第三个环中(,它们将保持多态性。
试图隔离效果时要记住的另一件事是,长期运行的功能将在其运行时被编译在后台,然后在堆栈上替换某个时候(" OSR"(,这会添加各种噪音为您的测量结果。当您以不同的循环长度进行热身调用它们时,它们仍然会在后台进行编译,但是无法可靠地等待该背景工作。您可以诉诸于用于开发的命令行旗帜,但是您将不再衡量常规行为。
无论如何,以下是尝试制作类似于您的测试的尝试,该测试产生了合理的结果(关于我的机器上的100 180 280
(:
function Point() {}
// These three functions are identical, but they will be called with different
// inputs and hence collect different type feedback:
function processPointMonomorphic(N, point) {
let sum = 0;
for (let i = 0; i < N; i++) {
sum += point.a;
}
return sum;
}
function processPointPolymorphic(N, point) {
let sum = 0;
for (let i = 0; i < N; i++) {
sum += point.a;
}
return sum;
}
function processPointGeneric(N, point) {
let sum = 0;
for (let i = 0; i < N; i++) {
sum += point.a;
}
return sum;
}
let p1 = new Point();
let p2 = new Point();
let p3 = new Point();
let p4 = new Point();
const warmup = 12000;
const N = 100000000;
let sum = 0;
p1.a = 1;
p2.b = 1;
p2.a = 1;
p3.c = 1;
p3.b = 1;
p3.a = 1;
p4.d = 1;
p4.c = 1;
p4.b = 1;
p4.a = 1;
processPointMonomorphic(warmup, p1);
processPointMonomorphic(1, p1);
let start_1 = Date.now();
sum += processPointMonomorphic(N, p1);
let t1 = Date.now() - start_1;
processPointPolymorphic(2, p1);
processPointPolymorphic(2, p2);
processPointPolymorphic(2, p3);
processPointPolymorphic(warmup, p4);
processPointPolymorphic(1, p4);
let start_2 = Date.now();
sum += processPointPolymorphic(N, p1);
let t2 = Date.now() - start_2;
processPointGeneric(warmup, 1);
processPointGeneric(1, 1);
let start_3 = Date.now();
sum += processPointGeneric(N, p1);
let t3 = Date.now() - start_3;
console.log(t1, t2, t3);