奇怪的JavaScript性能

时间:2015-04-02 23:14:44

标签: javascript performance

当我在JavaScript中实现ChaCha20时,我偶然发现了一些奇怪的行为。

我的第一个版本是这样构建的(让我们称之为" Encapsulated Version"):

function quarterRound(x, a, b, c, d) {
    x[a] += x[b]; x[d] = ((x[d] ^ x[a]) << 16) | ((x[d] ^ x[a]) >>> 16);
    x[c] += x[d]; x[b] = ((x[b] ^ x[c]) << 12) | ((x[b] ^ x[c]) >>> 20);
    x[a] += x[b]; x[d] = ((x[d] ^ x[a]) <<  8) | ((x[d] ^ x[a]) >>> 24);
    x[c] += x[d]; x[b] = ((x[b] ^ x[c]) <<  7) | ((x[b] ^ x[c]) >>> 25);
}

function getBlock(buffer) {
    var x = new Uint32Array(16);

    for (var i = 16; i--;) x[i] = input[i];
    for (var i = 20; i > 0; i -= 2) {
        quarterRound(x, 0, 4, 8,12);
        quarterRound(x, 1, 5, 9,13);
        quarterRound(x, 2, 6,10,14);
        quarterRound(x, 3, 7,11,15);
        quarterRound(x, 0, 5,10,15);
        quarterRound(x, 1, 6,11,12);
        quarterRound(x, 2, 7, 8,13);
        quarterRound(x, 3, 4, 9,14);
    }
    for (i = 16; i--;) x[i] += input[i];
    for (i = 16; i--;) U32TO8_LE(buffer, 4 * i, x[i]);
    input[12]++;
    return buffer;
}

为了减少不必要的函数调用(带参数开销等),我删除了quarterRound - 函数并将其内容置于内联(它是正确的;我对一些测试向量进行了验证) :

function getBlock(buffer) {
    var x = new Uint32Array(16);

    for (var i = 16; i--;) x[i] = input[i];
    for (var i = 20; i > 0; i -= 2) {
        x[ 0] += x[ 4]; x[12] = ((x[12] ^ x[ 0]) << 16) | ((x[12] ^ x[ 0]) >>> 16);
        x[ 8] += x[12]; x[ 4] = ((x[ 4] ^ x[ 8]) << 12) | ((x[ 4] ^ x[ 8]) >>> 20);
        x[ 0] += x[ 4]; x[12] = ((x[12] ^ x[ 0]) <<  8) | ((x[12] ^ x[ 0]) >>> 24);
        x[ 8] += x[12]; x[ 4] = ((x[ 4] ^ x[ 8]) <<  7) | ((x[ 4] ^ x[ 8]) >>> 25);
        x[ 1] += x[ 5]; x[13] = ((x[13] ^ x[ 1]) << 16) | ((x[13] ^ x[ 1]) >>> 16);
        x[ 9] += x[13]; x[ 5] = ((x[ 5] ^ x[ 9]) << 12) | ((x[ 5] ^ x[ 9]) >>> 20);
        x[ 1] += x[ 5]; x[13] = ((x[13] ^ x[ 1]) <<  8) | ((x[13] ^ x[ 1]) >>> 24);
        x[ 9] += x[13]; x[ 5] = ((x[ 5] ^ x[ 9]) <<  7) | ((x[ 5] ^ x[ 9]) >>> 25);
        x[ 2] += x[ 6]; x[14] = ((x[14] ^ x[ 2]) << 16) | ((x[14] ^ x[ 2]) >>> 16);
        x[10] += x[14]; x[ 6] = ((x[ 6] ^ x[10]) << 12) | ((x[ 6] ^ x[10]) >>> 20);
        x[ 2] += x[ 6]; x[14] = ((x[14] ^ x[ 2]) <<  8) | ((x[14] ^ x[ 2]) >>> 24);
        x[10] += x[14]; x[ 6] = ((x[ 6] ^ x[10]) <<  7) | ((x[ 6] ^ x[10]) >>> 25);
        x[ 3] += x[ 7]; x[15] = ((x[15] ^ x[ 3]) << 16) | ((x[15] ^ x[ 3]) >>> 16);
        x[11] += x[15]; x[ 7] = ((x[ 7] ^ x[11]) << 12) | ((x[ 7] ^ x[11]) >>> 20);
        x[ 3] += x[ 7]; x[15] = ((x[15] ^ x[ 3]) <<  8) | ((x[15] ^ x[ 3]) >>> 24);
        x[11] += x[15]; x[ 7] = ((x[ 7] ^ x[11]) <<  7) | ((x[ 7] ^ x[11]) >>> 25);
        x[ 0] += x[ 5]; x[15] = ((x[15] ^ x[ 0]) << 16) | ((x[15] ^ x[ 0]) >>> 16);
        x[10] += x[15]; x[ 5] = ((x[ 5] ^ x[10]) << 12) | ((x[ 5] ^ x[10]) >>> 20);
        x[ 0] += x[ 5]; x[15] = ((x[15] ^ x[ 0]) <<  8) | ((x[15] ^ x[ 0]) >>> 24);
        x[10] += x[15]; x[ 5] = ((x[ 5] ^ x[10]) <<  7) | ((x[ 5] ^ x[10]) >>> 25);
        x[ 1] += x[ 6]; x[12] = ((x[12] ^ x[ 1]) << 16) | ((x[12] ^ x[ 1]) >>> 16);
        x[11] += x[12]; x[ 6] = ((x[ 6] ^ x[11]) << 12) | ((x[ 6] ^ x[11]) >>> 20);
        x[ 1] += x[ 6]; x[12] = ((x[12] ^ x[ 1]) <<  8) | ((x[12] ^ x[ 1]) >>> 24);
        x[11] += x[12]; x[ 6] = ((x[ 6] ^ x[11]) <<  7) | ((x[ 6] ^ x[11]) >>> 25);
        x[ 2] += x[ 7]; x[13] = ((x[13] ^ x[ 2]) << 16) | ((x[13] ^ x[ 2]) >>> 16);
        x[ 8] += x[13]; x[ 7] = ((x[ 7] ^ x[ 8]) << 12) | ((x[ 7] ^ x[ 8]) >>> 20);
        x[ 2] += x[ 7]; x[13] = ((x[13] ^ x[ 2]) <<  8) | ((x[13] ^ x[ 2]) >>> 24);
        x[ 8] += x[13]; x[ 7] = ((x[ 7] ^ x[ 8]) <<  7) | ((x[ 7] ^ x[ 8]) >>> 25);
        x[ 3] += x[ 4]; x[14] = ((x[14] ^ x[ 3]) << 16) | ((x[14] ^ x[ 3]) >>> 16);
        x[ 9] += x[14]; x[ 4] = ((x[ 4] ^ x[ 9]) << 12) | ((x[ 4] ^ x[ 9]) >>> 20);
        x[ 3] += x[ 4]; x[14] = ((x[14] ^ x[ 3]) <<  8) | ((x[14] ^ x[ 3]) >>> 24);
        x[ 9] += x[14]; x[ 4] = ((x[ 4] ^ x[ 9]) <<  7) | ((x[ 4] ^ x[ 9]) >>> 25);
    }
    for (i = 16; i--;) x[i] += input[i];
    for (i = 16; i--;) U32TO8_LE(buffer, 4 * i, x[i]);
    input[12]++;
    return buffer;
}

但是表现结果并不像预期的那样:

Encapsulated performance

VS

Inline performance

虽然Firefox和Safari下的性能差异可以忽略或不重要,但Chrome的性能下降还是巨大的...... 任何想法为什么会这样?

P.S。:如果图像很小,请在新标签中打开它们:)

PP.S。:以下是链接:

Inlined

Encapsulated

1 个答案:

答案 0 :(得分:23)

回归的发生是因为你遇到了V8当前优化编译器Crankshaft中的一个传递中的错误。

如果你看一下Crankshaft对缓慢的“内联”案例做了什么,你会注意到getBlock函数经常不优化。

要查看您可以将--trace-deopt标志传递给V8并读取它转储到控制台的输出或使用名为IRHydra的工具。

我为内联和非内联案例收集了V8输出,你可以在IRHydra中探索:

以下是“内联”案例的内容:

method list

功能列表中的每个条目都是单个优化尝试。红色意味着优化的函数后来被优化,因为违反了优化编译器做出的某些假设。

这意味着getBlock会不断优化和去优化。在“封装”案例中没有类似的东西:

enter image description here

此处getBlock优化一次,绝不会优化。

如果我们查看getBlock内部,我们会看到来自Uint32Array的数组加载已取消优化,因为此加载的结果是一个不符合int32值的值。

enter image description here

这种贬低的原因有点令人费解。 JavaScript唯一的数字类型是双精度浮点数。使用它进行所有计算会有些低效,因此优化JIT通常会尝试将整数值保持为优化代码中的实际整数。

Crankshaft的最宽整数表示为int32,其中一半uint32值无法表示。为了部分缓解这种限制,Crankshaft执行一个名为 uint32 analysis 的优化过程。此过程试图确定将uint32值表示为int32值是否安全 - 这是通过查看如何使用此uint32值来完成的:某些操作,例如按位,不关心“符号”但只关心单个位,可以教导其他操作(例如,从优化到双重的去优化或转换)以特殊方式处理int32-that-is-real-uint32。如果分析成功 - 所有使用uint32值都是安全的 - 那么此操作将以特殊方式标记,否则(发现某些用途不安全)操作未标记,并且如果生成{{ 1}}不符合uint32范围的值(高于int32的任何内容)。

在这种情况下,分析并未将0x7fffffff标记为安全的x[i]操作 - 因此当uint32的结果超出x[i]范围时,它会进行去优化。不将int32标记为安全的原因是因为其中一个用途,即内联x[i]时由内联创建的人工指令被认为是不安全的。这是一个解决问题的patch for V8,它还包含一个小问题:

U32TO8_LE

你没有在“封装”版本中遇到这个错误,因为Crankshaft自己的内联在到达var u32 = new Uint32Array(1); u32[0] = 0xFFFFFFFF; // this uint32 value doesn't fit in int32 function tr(x) { return x|0; // ^^^ - this use is uint32-safe } function ld() { return tr(u32[0]); // ^ ^^^^^^ uint32 op, will deopt if uses are not safe // | // \--- tr is inlined into ld and an hidden artificial // HArgumentObject instruction was generated that // captured values of all parameters at entry (x) // This instruction was considered uint32-unsafe // by oversight. } while (...) ld(); 呼叫站点之前已经用完了预算。正如您在IRHydra中所看到的,只有前三个对U32TO8_LE的调用内联:

enter image description here

您可以通过将quarterRound更改为U32TO8_LE(buffer, 4 * i, x[i])来解决此错误,该U32TO8_LE(buffer, 4 * i, x[i]|0)仅使用x[i]值uint32-safe并且不会更改结果。