2015-04-02 85 views
10

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

我的第一个版本是建立这样的(我们称之为“封装版本”):

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

Inline performance

而Firefox和Safari浏览器下的性能差异neglectible或不重要的Chrome下的性能切是巨大的...... 任何想法,为什么出现这种情况?

PS:如果图像是小,在一个新的标签:)

PP.S打开它们:这里是链接:

Inlined

Encapsulated

+0

评论是不适合扩展讨论;这个对话已经[转移到聊天](http://chat.stackoverflow.com/rooms/74430/discussion-on-question-by-k-biermann-strange-javascript-performance)。 – 2015-04-03 14:43:53

+0

1)创建数组的成本很高:重新使用相同的缓冲区。 2)告诉我们你的U32TO8_LE,这可能是昂贵的。 3)在quarterRound中,缓存所有值,进行数学计算,然后存储结果。这里的高收益,我猜(8阵列间接而不是... 28!)。 4)你也可以考虑将8个函数与相关参数绑定,只将x更改为最后一个参数而不是第一个参数。非常肯定的是,所有这些表演将会飞涨。 – GameAlchemist 2015-04-10 13:17:42

回答

21

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

如果您看看曲轴对慢速“内联”情况的作用,您会注意到getBlock函数会不断地去最优化。

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

我收集了两个内联和uninlined案件V8输出,可以在IRHydra探索:

下面是它显示了“内联”情况:

method list

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

这意味着getBlock不断优化和去最佳化。有没有像这样的“封装”的情况:

enter image description here

这里getBlock优化一次,从来没有deoptimizes。

如果我们看一下里面getBlock我们将看到它是从Uint32Array是deoptimizes因为这种负荷的结果是不适合int32值的值阵列负载。

enter image description here

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

曲轴最宽的整数表示形式为int32,其中一半的值不能在其中表示。为了部分缓解这种限制,曲轴执行称为uint32分析的优化通道。此通道试图找出将uint32值表示为int32值是否安全 - 这是通过查看如何使用此值来完成的:某些操作(例如,并不关心“符号”,而只关心个别位,其他操作(例如去优化或从整数到双精度的转换)可以教导以特殊方式处理int32-that-is-actually-uint32。如果分析成功 - 所有使用的值都是安全的 - 那么这个操作用特殊方式标记,否则(有些用途被发现是不安全的)操作没有标记,如果它产生的值不合适到int32范围内(任何高于0x7fffffff的值)。

在这种情况下,分析未标记x[i]作为一种安全uint32操作 - 所以有人去优化时的x[i]结果的int32范围之外。没有将x[i]标记为安全的原因是因为它的一个用途,即内嵌者在内联时创建的虚假指令被认为是不安全的。这里是一个patch for V8修复它也包含了问题的一个小例证问题:

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(); 

因为曲轴自身的内衬快用完了预算就达到U32TO8_LE调用之前你没有打在“封装”版本的bug现场。正如你在IRHydra只能看到前三个调用quarterRound内联:

enter image description here

你可以通过改变U32TO8_LE(buffer, 4 * i, x[i])解决此漏洞以U32TO8_LE(buffer, 4 * i, x[i]|0)这使得x[i]价值的唯一使用UINT32安全的,不会改变结果。