是什么让这个功能运行得慢得多?

时间:2015-07-29 10:46:59

标签: javascript performance v8

我一直在尝试进行实验,看看函数中的局部变量是否存储在堆栈中。

所以我写了一点性能测试

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
}

我希望能够更快地完成逆功能工作。取而代之的是一个惊人的结果。

直到我测试它运行的一个函数比测试第二个函数快10倍。

示例:

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

以其他顺序测试时的行为相同。

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

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

什么可能是这种显着(约12倍)性能的来源?

PS。由于它似乎仅适用于某些环境,请编写您正在使用的环境进行测试。

我是:

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

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

/ EDIT2
将功能内联到测试功能使其始终同时运行 是否可能有一个优化内联函数参数,如果它始终是相同的函数?

3 个答案:

答案 0 :(得分:102)

使用两个不同的函数调用test fn()内部的callsite变为megamorphic,V8无法内联。

V8中的函数调用(与方法调用o.m(...)相反)伴随着一个元素内联缓存,而不是真正的多态内联缓存。

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

enter image description here

V8刚刚内联straight删除了您希望通过Dead Code Elimination优化进行基准测试的所有代码。在V8的旧版本而不是DCE V8上只需通过LICM将代码提升出循环 - 因为代码是完全循环不变的。

straight没有内联时,V8无法应用这些优化 - 因此性能差异。较新版本的V8仍会将DCE应用于straightinversed自己将其转换为空函数

enter image description here

因此性能差异不大(约2-3倍)。较旧的V8对于DCE来说不够具有侵略性 - 并且在内联和非内联案例中表现出更大的差异,因为内联案例的峰值性能仅仅是积极的循环不变代码运动(LICM)的结果。

在相关的说明中,这表明为什么基准测试永远不应该这样写 - 因为他们的结果没有任何用处,因为你最终测量一个空循环。

如果您对多态性及其在V8中的含义感兴趣,请查看我的帖子"What's up with monomorphism"(“并非所有缓存都相同”部分讨论与函数调用相关的缓存)。我还建议阅读我关于微基准测试危险的一个讨论,例如: 2015年GOTO芝加哥("Benchmarking JS")最近的video讲话 - 它可以帮助您避免常见的陷阱。

答案 1 :(得分:17)

你误解了堆栈。

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

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节省大量工作。

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

答案 2 :(得分:3)

  

将功能内联到测试功能使其始终同时运行   是否可能有一个优化内联函数参数,如果它始终是相同的函数?

是的,这似乎正是您所观察到的。正如@Luaan已经提到的,编译器可能会丢弃straightinverse函数的主体,因为它们没有任何副作用,只是操纵一些局部变量。

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

但是当您使用不同的test来呼叫fn时,它必须进行去优化。它可能稍后再做一些其他优化,但现在知道有两个不同的函数需要调用它不能再内联它们。

由于您实际测量的唯一因素是函数调用,因此会导致结果出现严重差异。

  

查看函数中的局部变量是否存储在堆栈中的实验

关于你的实际问题,不,单个变量不存储在堆栈(stack machine)上,而是存储在寄存器(register machine)中。它们在您的函数中声明或使用的顺序无关紧要。

然而,它们存储在the stack上,作为所谓的“堆栈帧”的一部分。每个函数调用都有一个帧,存储其执行上下文的变量。在您的情况下,堆栈可能如下所示:

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