Ответ 1
Регрессия происходит из-за того, что вы нажмете ошибку в одном из проходов в текущем оптимизационном компиляторе V8 коленчатого вала.
Если вы посмотрите, что делает коленчатый вал для медленного "вложенного" случая, вы заметите, что функция getBlock
постоянно деоптимизирует.
Чтобы увидеть, что вы можете просто передать флаг --trace-deopt
в V8 и прочитать вывод, который он выгружает на консоль, или использовать инструмент IRHydra.
Я собрал V8-выход для вложенных и неизолированных случаев, вы можете исследовать в IRHydra:
- вложенный случай: http://mrale.ph/irhydra/2/#gist:1fefdc70567a924d4cf2
- инкапсулированный случай: http://mrale.ph/irhydra/2/#gist:be3de64f860c78ca640b
Вот что он показывает для "вложенного" случая:
Каждая запись в списке функций представляет собой единственную попытку оптимизации. Красный цвет означает, что оптимизированная функция позже деоптимизируется, потому что какое-то допущение, сделанное оптимизирующим компилятором, было нарушено.
Это означает, что getBlock
постоянно оптимизируется и деоптимизируется. В "инкапсулированном" случае нет ничего подобного:
Здесь getBlock
оптимизируется один раз и никогда не деоптимизирует.
Если мы заглянем внутрь getBlock
, мы увидим, что это загрузка массива из Uint32Array
, которая деоптимизирует, потому что результатом этой загрузки является значение, которое не соответствует значению int32
.
Причины этого deopt немного запутаны. Тип только для JavaScript - это число с плавающей запятой двойной точности. Выполнение всех вычислений с ним было бы несколько неэффективным, поэтому оптимизация JIT обычно пытается сохранить целые значения, представленные как фактические целые числа в рамках оптимизированного кода.
Максимальное целочисленное представление коленчатого вала int32
, а половина значений uint32
в нем не представляются. Чтобы частично смягчить это ограничение, Crankshaft выполняет оптимизационный проход, называемый uint32. Этот проход пытается определить, можно ли представлять значение uint32
как значение int32
, безопасно ли это представлять, как это делается, глядя на то, как это значение uint32
используется: некоторые операции, например. поразрядные, не заботятся о "знаке", а только о отдельных битах, другие операции (например, деоптимизация или преобразование из целого числа в double) можно научить обрабатывать int32-that-is-actual-uint32 особым образом. Если анализ преуспевает - все использование значения uint32
безопасно - тогда эта операция отмечена специальным образом, в противном случае (некоторые виды использования считаются небезопасными) операция не отмечена и будет отменена, если она произведет значение uint32
который не вписывается в диапазон int32
(что-либо выше 0x7fffffff
).
В этом случае анализ не маркировал x[i]
как безопасную операцию uint32
, поэтому он был деоптимизирован, когда результат x[i]
находился вне диапазона int32
. Причиной не маркировки x[i]
как безопасности было то, что одно из его применений - искусственная инструкция, созданная inliner при встраивании 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();
Вы не попали в эту ошибку в "инкапсулированной" версии, потому что у Inliner у коленчатого вала не хватало бюджета, прежде чем он достигло сайта вызова U32TO8_LE
. Как вы можете видеть в IRHydra, только первые три вызова quarterRound
заключены в очередь:
Вы можете обойти эту ошибку, изменив U32TO8_LE(buffer, 4 * i, x[i])
на U32TO8_LE(buffer, 4 * i, x[i]|0)
, что делает единственное использование x[i]
значения uint32-safe и не изменяет результат.