当我在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;
}
但是表现结果并不像预期的那样:
VS
虽然Firefox和Safari下的性能差异可以忽略或不重要,但Chrome的性能下降还是巨大的...... 任何想法为什么会这样?
P.S。:如果图像很小,请在新标签中打开它们:)
PP.S。:以下是链接:
答案 0 :(得分:23)
回归的发生是因为你遇到了V8当前优化编译器Crankshaft中的一个传递中的错误。
如果你看一下Crankshaft对缓慢的“内联”案例做了什么,你会注意到getBlock
函数经常不优化。
要查看您可以将--trace-deopt
标志传递给V8并读取它转储到控制台的输出或使用名为IRHydra的工具。
我为内联和非内联案例收集了V8输出,你可以在IRHydra中探索:
以下是“内联”案例的内容:
功能列表中的每个条目都是单个优化尝试。红色意味着优化的函数后来被优化,因为违反了优化编译器做出的某些假设。
这意味着getBlock
会不断优化和去优化。在“封装”案例中没有类似的东西:
此处getBlock
优化一次,绝不会优化。
如果我们查看getBlock
内部,我们会看到来自Uint32Array
的数组加载已取消优化,因为此加载的结果是一个不符合int32
值的值。
这种贬低的原因有点令人费解。 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
的调用内联:
您可以通过将quarterRound
更改为U32TO8_LE(buffer, 4 * i, x[i])
来解决此错误,该U32TO8_LE(buffer, 4 * i, x[i]|0)
仅使用x[i]
值uint32-safe并且不会更改结果。