K. *_*ann 12 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;
}
Run Code Online (Sandbox Code Playgroud)
为了减少不必要的函数调用(带参数开销等),我删除了quarterRound-function并将其内容置于内联(这是正确的;我对一些测试向量进行了验证):
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;
}
Run Code Online (Sandbox Code Playgroud)
但性能结果并不如预期:

与

虽然Firefox和Safari下的性能差异是可以忽略不计或不重要的,但Chrome下的性能削减是巨大的......任何想法为何会出现这种情况?
PS:如果图像很小,请在新标签中打开它们:)
PP.S.:以下是链接:
Vya*_*rov 24
回归的发生是因为你遇到了V8当前优化编译器Crankshaft中的一个传递中的错误.
如果你看一下Crankshaft对缓慢的"内联"情况做了什么,你会注意到这个getBlock功能不断优化.
要查看您可以将--trace-deopt标志传递给V8并读取它转储到控制台的输出或使用名为IRHydra的工具.
我为内联和非内联案例收集了V8输出,您可以在IRHydra中进行探索:
以下是"内联"案例的内容:

功能列表中的每个条目都是单个优化尝试.红色意味着优化的函数后来被优化,因为违反了优化编译器做出的某些假设.
这种方法getBlock不断优化和去优化.在"封装"案例中没有这样的事情:
这里getBlock优化一次,从不优化.
如果我们查看内部,getBlock我们将看到它是来自Uint32Arraydeoptimizes的数组加载,因为此加载的结果是一个不适合int32值的值.
这种贬低的原因有点复杂.JavaScript唯一的数字类型是双精度浮点数.用它进行所有计算会有些低效,因此优化JIT通常会尝试将整数值保持在优化代码中表示为实际整数.
Crankshaft的最宽整数表示是int32其中一半的uint32值不可表示.为了部分缓解这种限制,Crankshaft执行一个名为uint32分析的优化过程.这个过程试图弄清楚uint32将int32值表示为值是否安全- 这是通过查看如何使用该uint32值来完成的:某些操作(例如按位)不关心"符号"而只关心单个位,可以教授其他操作(例如,从优化到双重的去优化或转换)以特殊方式处理int32-that-is-actual-uint32.如果分析成功 - 所有uint32值的使用都是安全的 - 那么这个操作会以特殊的方式标记,否则(某些用途被发现是不安全的)操作没有被标记,并且如果它产生的uint32值不符合int32范围则会取消(以上任何内容0x7fffffff).
在这种情况下,分析并不是标记x[i]为安全uint32操作 - 因此当结果x[i]超出int32范围时,它会被去优化.不标记x[i]为安全的原因是因为它的一个用途是内联在内联时创建的人工指令U32TO8_LE被认为是不安全的.这是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();
Run Code Online (Sandbox Code Playgroud)
你没有在"封装"版本中遇到这个错误,因为Crankshaft自己的内联在它到达U32TO8_LE呼叫站点之前已经用完了预算.正如您在IRHydra中所看到的,只有前三个quarterRound内联调用:
您可以通过更改U32TO8_LE(buffer, 4 * i, x[i])为U32TO8_LE(buffer, 4 * i, x[i]|0)唯一使用x[i]值uint32-safe 来解决此错误,并且不会更改结果.