max = 100000000000;
console.log(' max=', max);
let i = 0;
while(i < max) {
const y = Math.pow(2,100);
const z = Math.pow(15, 100000);
i++;
}
console.timeEnd("answer time");
Вот эта штука крутится почти две минуты. И даже убрать тело цикла в функцию, чтобы гарантированно попасть в кэши JIT никак не помогает. Неужели JIT настолько плох, что не может оптимизировать бесполезные вызовы?
Что же, открываем Deopt Explorer и ищем проблему. На самом деле, вот этот код будет тормозить точно так же:
const max = 100000000000;
let i = 0;
while(i < max) {
i++;
}
Deopt Explorer сразу даёт нам причину деоптимизации — i++ (overflow). Что за дела, спросите вы? Это же далеко не Number.MAX_SAFE_INTEGER, запаса достаточно. Дело в том, что V8 (как и другие JS-движки) умеет эффективно работать со SMI (small integers). На 64-битных платформах это соответственно диапазон от -2³¹ до 2³¹-1.
Проверим?
%DebugPrint(2147483647);
DebugPrint: Smi: 0x7fffffff (2147483647)
А вот мы вышли за границы SMI:
%DebugPrint(2147483648);
DebugPrint: 2147483648.0
0x148993d415e9: [Map] in ReadOnlySpace
- type: HEAP_NUMBER_TYPE
- instance size: 16
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x148993d415b9 <undefined>
- prototype_validity cell: 0
- instance descriptors (own) #0: 0x148993d41269 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x148993d41339 <null>
- constructor: 0x148993d41339 <null>
- dependent code: 0x148993d41251 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
Такой подход называется pointer tagging — за счёт отдельного бита мы можем точно сказать, что в данном случае у нас не указатель на heap, а непосредственно значение примитива. Т.е. брать значение прямо из стека, а не бегать за ним в кучу. Получается, что как только мы выходим за границу SMI мы уже начинаем работать с объектом в куче и теряем в производительности.
В итоге как обычно попали в ловушку синтетических тестов и протестировали не то, что хотели протестировать, но многое поняли :)
UPD
Бенчмарки для оригинального цикла: 1:35.161 (m:ss.mmm)
Для решения с вложенным циклом (чтобы указатели остались в smi): 48.156s
А если мы поможем JIT и вложенный цикл уберём в функцию, то: 32.325s
Похожая ситуация с JVM и размером Heap, если выходишь за 32G то все хуже работает. Потому что Compressed Pointer Headers выключаются на размерах больше 32
Обсуждают сегодня