是什么让这个函数 运行 变慢了?

What makes this function run much slower?

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

所以我写了一点性能测试

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。由于它似乎只在某些环境中工作,请写下您用来测试它的环境。

我的是:

OS: Ubuntu 14.04
节点 v0.10.37
Chrome 43.0.2357.134(正式版)(64 位)

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

/编辑2
将函数内联到测试函数使其 运行 始终相同。
如果函数参数始终是同一个函数,是否有可能进行内联函数参数的优化?

你误解了堆栈。

虽然 "real" 堆栈确实只有 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.

节省大量工作。

为了回答您的实际问题,我怀疑这两个函数实际上都没有做任何事情。您只会修改局部变量,而您的函数不会返回任何东西——编译器完全删除函数体,甚至可能删除函数调用是完全合法的。如果确实如此,那么您观察到的任何性能差异都可能只是一个测量工件,或者与调用函数/迭代的固有成本相关的东西。

Inlining the function to the test function makes it run always the same time.
Is it possible that there is an optimization that inlines the function parameter if it's always the same function?

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

当你第一次调用 test(…, 100000) 时,优化编译器经过一些迭代后意识到被调用的 fn() 总是相同的,并内联它,避免了昂贵的函数调用.它现在所做的就是将变量递减 1000 万次并针对 0.

进行测试

但是当你用不同的 fn 调用 test 时,它必须取消优化。它稍后可能会再次进行一些其他优化,但现在知道要调用两个不同的函数,它不能再将它们内联。

由于您真正要衡量的唯一事情是函数调用,因此这会导致您的结果出现重大差异。

An experiment to see if the local variables in functions are stored on a stack

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

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

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

一旦你用两个不同的函数调用 test fn() callsite inside 它就会变成 megamorphic 并且 V8 无法内联它。

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

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

V8 刚刚内联 straight 并且 删除了 您希望通过死代码消除优化进行基准测试的所有代码。在旧版本的 V8 而不是 DCE 上,V8 只会通过 LICM 将代码提升到循环之外——因为代码是完全循环不变的。

straight 未内联时,V8 无法应用这些优化 - 因此存在性能差异。较新版本的 V8 仍会将 DCE 应用于 straightinversed 本身将它们变成空函数

所以性能差异并不大(大约 2-3 倍)。较旧的 V8 对 DCE 不够激进——这将在内联和非内联案例之间表现出更大的差异,因为内联案例的峰值性能完全是激进的循环不变代码运动 (LICM) 的结果。

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

如果您对多态性及其在 V8 中的影响感兴趣,请查看我的 post "What's up with monomorphism" (section "Not all caches are the same" talks about the caches associated with function calls). I also recommend reading through one of my talks about dangers of microbenchmarking, e.g. most recent "Benchmarking JS" talk from GOTO Chicago 2015 (video) - 它可能会帮助您避免常见的陷阱。