Студент прислал вопрос — почему этот код тормозит?





console.time ("answer time");

const 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