Как V8 работает с числами. Small Integer теория
В JavaScript по спецификации все числа (
Что такое Smi
Smi — это способ представления небольших целых чисел без выделения памяти в куче:
- Smi хранятся прямо в указателе
- Могут занимать 31 бит (на 32-битных системах) или 32 бита (на 64-битных системах)
- Диапазон значений для 64 битной системы: от
Формат хранения для разных систем:
Smi всегда заканчивается нулевым битом (
Заглянем в исходники V8
Формат Smi описан в исходниках:
- smi.h
- v8-internal.h
Первое на что можно обратить внимание, задание
Отмечу, что несколькими строками ниже, лежит всеми любимая путаница про "примитивные типы". Однако, как в исходном коде, так и в спецификации, упоминаются
И можно найти константы для работы с тегом плюс маску для проверки:
### Как определяется диапазон значений Smi
Границы Smi заданы так:
Как это соответствует
Для получения значения
Мы видел, что класс Smi статичный
Это означает, что его нельзя создать через
Обратимся к определению struct
И так мы получаем формулу для 64 битной системы:
Сначала выполняется
После этого сдвигаем на
Получаем значением
Подведем итог
Smi:
- не требуют аллокации памяти
- не создают объект HeapNumber
- позволяют JIT-компилятору делать простые арифметические операции без проверок
Это особенно важно внутри циклов или горячих участков кода. Рассмотрим это в практической части в новых постах.
---
#V8 #JavaScript #Smi #Performance #JSразбор
В JavaScript по спецификации все числа (
number) — это 64-битные числа с плавающей запятой (IEEE-754 Double). Но внутри V8 используется множество стратегий, чтобы сделать работу с числами быстрее. Одна из таких — Smi (Small Integer).Что такое Smi
Smi — это способ представления небольших целых чисел без выделения памяти в куче:
- Smi хранятся прямо в указателе
- Могут занимать 31 бит (на 32-битных системах) или 32 бита (на 64-битных системах)
- Диапазон значений для 64 битной системы: от
-2^31 до 2^31 - 1.Формат хранения для разных систем:
// для 32-битной системы
[31 bit signed int] 0
// для 64-битной системы
[32 bit signed int] [31 bit zero padding] 0
Smi всегда заканчивается нулевым битом (
LSB = 0) — это так называемый тег-бит, отличающий Smi от указателей на другие объекты, у которых LSB = 1. Подробнее об этом в теме Tagged Pointer.Заглянем в исходники V8
Формат Smi описан в исходниках:
- smi.h
- v8-internal.h
Первое на что можно обратить внимание, задание
alias для типа Number.using Number = Union<Smi, HeapNumber>;
Отмечу, что несколькими строками ниже, лежит всеми любимая путаница про "примитивные типы". Однако, как в исходном коде, так и в спецификации, упоминаются
primitive value.// A primitive JavaScript value, which excludes JS objects.
using JSPrimitive =
Union<Smi, HeapNumber, BigInt, String, Symbol, Boolean, Null, Undefined>;
И можно найти константы для работы с тегом плюс маску для проверки:
// Tag information for Smi.
const int kSmiTag = 0; // значение тега для Smi (малых целых чисел)
const int kSmiTagSize = 1; // размер тега в битах (1 бит)
const intptr_t kSmiTagMask = (1 << kSmiTagSize) - 1; // маска для выделения тега из указателя (равна 1)
### Как определяется диапазон значений Smi
Границы Smi заданы так:
static const int kSmiMinValue =
(static_cast<unsigned int>(-1)) << (kSmiValueSize — 1);
static const int kSmiMaxValue = -(kSmiMinValue + 1);
Как это соответствует
-2³¹ и 2³¹-1? Разберёмся с этим по порядку.Для получения значения
kSmiValueSize нам снова необходимо обратиться к коду.Мы видел, что класс Smi статичный
class Smi : public AllStatic
Это означает, что его нельзя создать через
new и в классе будут только статичные методы.Обратимся к определению struct
SmiTagging. В коде их две для разных платформ и в них же записан enum с необходимыми константами. kSmiShiftSize - на сколько бит сдвигается значение и kSmiValueSize количество битов, выделенных под значением. В структуру же передается размер указателя в байтах 4 (32 бита) или 8 (64 бита).struct SmiTagging<4> {
enum { kSmiShiftSize = 0, kSmiValueSize = 31 };
...
}
struct SmiTagging<8> {
enum { kSmiShiftSize = 31, kSmiValueSize = 32 };
...
}
И так мы получаем формулу для 64 битной системы:
(static_cast<unsigned int>(-1)) << (32 - 1)
Сначала выполняется
cast значения -1 к беззнаковому виду и становится 0xFFFFFFFF (все 32 бита равны 1). Для понимания можно ознакомиться с представлением отрицательных целых чисел в беззнаковом виде (Two's complement).После этого сдвигаем на
31 бит:0xFFFFFFFF << 31
10000000 00000000 00000000 00000000
0x80000000 = 2147483648
Получаем значением
int (знаковое), то минимальная граница smi чисел равна -2147483648. Убираем знак и добавляем 1 и получаем верхнюю границу 2147483647Подведем итог
Smi:
- не требуют аллокации памяти
- не создают объект HeapNumber
- позволяют JIT-компилятору делать простые арифметические операции без проверок
Это особенно важно внутри циклов или горячих участков кода. Рассмотрим это в практической части в новых постах.
---
#V8 #JavaScript #Smi #Performance #JSразбор
🔥13🤩2
HeapNumber в V8. Как хранятся числа вне Smi. Теория часть 1
Продолжим рассматривать способы хранения чисел во время выполнения кода внутри V8. Когда движок сталкивается с числом, выходящим за пределы диапазона
Что такое HeapNumber
Когда возникает HeapNumber
Любые JavaScript
Внутреннее устройство HeapNumber
Обратимся к исходникам:
- src/objects/heap-number.h
- src/objects/primitive-heap-object.h
Сам класс
Он не содержит собственных полей или методов — его задача типизации. Это маркер, который позволяет компилятору и внутренним шаблонам
В
Обозначения:
- S — знаковый бит (1 = отрицательное значение)
- E — экспонента (11 бит)
- M — мантисса (52 бита в сумме)
Это деление соответствует стандарту IEEE-754 и отражает, как double хранится в памяти. Такое представление упрощает доступ к частям числа для анализа, компиляции и оптимизаций.
Помним и про порядок байт (endianness). Чаще используется Little-endian (x86-64, ARM64): нижнее слово хранится по меньшему адресу. Но есть и Big-endian
Внутреннее устройство констант
Для работы с
Маска для извлечения знакового бита из старшего 32-битного слова. Если бит установлен, число отрицательное:
Маска для извлечения всех 11 бит экспоненты из старшего слова:
Маска для извлечения верхних 20 бит мантиссы из старшего слова (остальные 32 бита мантиссы находятся в младшем слове):
К интересному можно отнести проверку на
Проверка на
- Если экспонента максимальна, а мантисса = 0, то это
- Если экспонента максимальна, а мантисса ≠ 0 — это
---
#V8 #JavaScript #HeapNumber #IEEE754 #JSразбор
Продолжим рассматривать способы хранения чисел во время выполнения кода внутри V8. Когда движок сталкивается с числом, выходящим за пределы диапазона
Smi, он создаёт HeapNumber — полноценный объект в куче.Что такое HeapNumber
HeapNumber — это 64-битное число с плавающей точкой, завёрнутое («упакованное») в объект на куче. Такое представление часто называют boxed double, поскольку значение double не может храниться напрямую в регистрах или указателях и оборачивается (boxing) в отдельную структуру с типовой информацией (Map) и полем для самого значения.Когда возникает HeapNumber
Любые JavaScript
Number, выходящие за пределы Smi-диапазона: содержащие дробную часть, ±Infinity, NaN, −0, а также любые результаты арифметических операций, приводящие к потере точности, переполнению или переходу в формат double (например, деление двух целых чисел с нецелым результатом).Внутреннее устройство HeapNumber
Обратимся к исходникам:
- src/objects/heap-number.h
- src/objects/primitive-heap-object.h
Сам класс
HeapNumber наследуется от PrimitiveHeapObject. Чтобы отделить «примитивы-объекты» (числа, BigInt, WasmNumber, String, но не Smi) от обычных JavaScript-объектов (таких как JSArray, JSFunction и т.д.), в V8 ввели промежуточный абстрактный класс PrimitiveHeapObject.Он не содержит собственных полей или методов — его задача типизации. Это маркер, который позволяет компилятору и внутренним шаблонам
V8 статически проверять, что конкретный класс представляет примитивное значение, не содержит тегированных ссылок (tagged pointers).В
V8 64-битное значение внутри double обрабатывается как два 32-битных слова. Это позволяет повысить эффективность операций, особенно на архитектурах с ограниченной поддержкой 64-битных инструкций или в оптимизированных путях компиляции.| high word | low word |
|(32 бита) |(32 бита) |
| ----------------- | ---------------|
| S (1 бит) | M-high (20 бит)|
| E (11 бит) | |
| M-low (32 бита) | |
Обозначения:
- S — знаковый бит (1 = отрицательное значение)
- E — экспонента (11 бит)
- M — мантисса (52 бита в сумме)
Это деление соответствует стандарту IEEE-754 и отражает, как double хранится в памяти. Такое представление упрощает доступ к частям числа для анализа, компиляции и оптимизаций.
Помним и про порядок байт (endianness). Чаще используется Little-endian (x86-64, ARM64): нижнее слово хранится по меньшему адресу. Но есть и Big-endian
Внутреннее устройство констант
Для работы с
HeapNumber V8 определяет несколько констант и масок:Маска для извлечения знакового бита из старшего 32-битного слова. Если бит установлен, число отрицательное:
static const uint32_t kSignMask = 0x80000000u;
// 10000000 00000000 00000000 00000000
Маска для извлечения всех 11 бит экспоненты из старшего слова:
static const uint32_t kExponentMask = 0x7ff00000u;
// 01111111 11110000 00000000 00000000
Маска для извлечения верхних 20 бит мантиссы из старшего слова (остальные 32 бита мантиссы находятся в младшем слове):
static const uint32_t kMantissaMask = 0xfffffu;
// 00000000 00001111 11111111 11111111
К интересному можно отнести проверку на
Infinity и NaNstatic const int kInfinityOrNanExponent =
(kExponentMask >> kExponentShift) - kExponentBias;
Проверка на
Infinity и NaN важна, потому что эти значения имеют одинаковую структуру на уровне IEEE-754: у них устанавливаются все биты экспоненты (11 битов равны 1), но различаются значения мантиссы:- Если экспонента максимальна, а мантисса = 0, то это
±Infinity. - Если экспонента максимальна, а мантисса ≠ 0 — это
NaN.---
#V8 #JavaScript #HeapNumber #IEEE754 #JSразбор
🔥5