是什么让这个function运行速度慢得多?

我一直试图做一个实验,看看函数中的局部variables是否存储在堆栈中。

于是我写了一点性能testing

function test(fn, times){ var i = times; var t = Date.now() while(i--){ fn() } return Date.now() - t; } ene function straight(){ var a = 1 var b = 2 var c = 3 var d = 4 var e = 5 a = a * 5 b = Math.pow(b, 10) c = Math.pow(c, 11) d = Math.pow(d, 12) e = Math.pow(e, 25) } function inversed(){ var a = 1 var b = 2 var c = 3 var d = 4 var e = 5 e = Math.pow(e, 25) d = Math.pow(d, 12) c = Math.pow(c, 11) b = Math.pow(b, 10) a = a * 5 } 

我希望得到相反的function工作更快。 相反,一个惊人的结果出来了。

直到我testing其中一个function运行速度比testing第二个快10倍。

例:

 > test(straight, 10000000) 30 > test(straight, 10000000) 32 > test(inversed, 10000000) 390 > test(straight, 10000000) 392 > test(inversed, 10000000) 390 

按照替代顺序进行testing时的相同行为

 > test(inversed, 10000000) 25 > test(straight, 10000000) 392 > test(inversed, 10000000) 394 

我已经在Chrome浏览器和Node.js中对它进行了testing,我完全不知道为什么会发生这种情况。 效果持续到我刷新当前页面或重新启动节点REPL。

什么可能是如此重大的(〜12倍)performance的来源?

PS。 由于它似乎只在一些环境中工作,请写下您正在使用的环境来testing它。

我的是:

操作系统:Ubuntu 14.04
节点v0.10.37
Chrome 43.0.2357.134(官方版本)(64位)

/编辑
在Firefox 39上,无论顺序如何,每次testing都需要约5500毫秒的时间。 这似乎只发生在特定的引擎上。

/ EDIT2
将函数内联到testing函数使得它始终运行在同一时间。
如果函数参数始终是相同的函数,是否有可能使函数参数内联的优化?

一旦你用两个不同的函数调用testfn()内部的callsite变成megamorphic,而V8不能内联。

函数调用(而不是方法调用om(...) )在V8中伴随着一个元素内联caching,而不是一个真正的多态内联caching。

由于V8无法在fn()调用站点内联,因此无法对您的代码应用各种优化。 如果您在IRHydra中查看您的代码(我上传了符合您需要的编译工件),您会注意到第一个优化的test版本(专用于fn = straight )有一个完全空的主循环。

在这里输入图像说明

V8只是内联,并删除了所有您希望通过Dead Code Elimination优化进行基准testing的代码。 在V8的旧版本而不是DCE V8上,只需通过LICM将代码从循环中提取出来 – 因为代码完全是循环不变的。

straight没有内联V8不能应用这些优化 – 因此性能的差异。 较新版本的V8仍然会将DCE应用于straight ,并将其转化为空函数

在这里输入图像说明

所以性能差异不大(2-3倍左右)。 较早的V8对于DCE来说不够积极 – 这将performance出内联和非内联之间的较大差异,因为内联情况下的最高性能仅仅是积极的循环不变代码运动(LICM)的结果。

在相关说明中,这显示了为什么基准testing不应该这样写 – 因为当你最终测量一个空循环时,它们的结果没有任何用处。

如果您对V8中的多态性及其含义感兴趣,请查看我的post“What is up with monomorphism” (“不是所有的caching都是一样的”都谈到与函数调用相关的caching)。 我还build议阅读我的一篇关于microbenchmarking的危害的讨论,例如GOTO Chicago 2015( video )中最新的“Benchmarking JS”演讲 – 这可能会帮助你避免常见的陷阱。

你误解了堆栈

虽然“真正的”堆栈确实只有PushPop操作,但这并不适用于执行的堆栈。 除了PushPop之外,只要有地址,你也可以随意访问任何variables。 这意味着即使编译器不为您重新sorting,本地的顺序也无关紧要。 在伪汇编中,你似乎认为

 var x = 1; var y = 2; x = x + 1; y = y + 1; 

翻译成类似的东西

 push 1 ; x push 2 ; y ; get y and save it pop tmp ; get x and put it in the accumulator pop a ; add 1 to the accumulator add a, 1 ; store the accumulator back in x push a ; restore y push tmp ; ... and add 1 to y 

事实上,真正的代码更像这样:

 push 1 ; x push 2 ; y add [bp], 1 add [bp+4], 1 

如果线程堆栈真的是一个真正的,严格的堆栈,这将是不可能的,真实的。 在这种情况下,行动和当地人的秩序比现在要重要得多。 相反,通过允许随机访问堆栈中的值,可以为编译器和CPU节省大量工作。

为了回答你的实际问题,我怀疑这两个function都没有做任何事情。 你只修改当地人,你的函数没有返回任何东西 – 编译器完全合法地删除函数体,甚至可能是函数调用。 如果确实如此,无论你观察到的性能差异可能只是一个测量工件,或与调用函数/迭代的固有成本相关的东西。

将函数内联到testing函数使得它始终运行在同一时间。
如果函数参数始终是相同的函数,是否有可能使函数参数内联的优化?

是的,这似乎正是你所观察到的。 正如@Luaan已经提到的那样,编译器可能会抛弃straight函数和inverse函数的主体,因为它们没有任何副作用,只能操作一些局部variables。

当您第一次调用test(…, 100000)时,优化编译器会在迭代后实现被调用的fn()始终相同,并将其内联,从而避免代价高昂的函数调用。 现在它所做的只是1000万次递减一个variables,并对0进行testing。

但是当你用不同的fn test时,它必须去优化。 它可能稍后再做一些其他的优化,但是现在知道有两个不同的函数可以被调用,所以不能再内联它们。

因为你唯一真正测量的是函数调用,这导致了结果的严重差异。

一个实验,看看函数中的局部variables是否存储在堆栈中

关于你的实际问题,不,单个variables不存储在堆栈( 堆栈机器 )中,而是存储在寄存器( 注册机器 )中。 它们在函数中声明或使用的顺序无关紧要。

然而,它们作为所谓的“堆栈帧”的一部分存储在堆栈中 。 每个函数调用你将有一个框架,存储它的执行上下文的variables。 在你的情况下,堆栈可能看起来像这样:

 [straight: a, b, c, d, e] [test: fn, times, i, t] …