8BitJS
158 subscribers
17 links
Modern JavaScript. Retro Spirit
Download Telegram
​​Как V8 работает с числами. Small Integer теория

В 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. Когда движок сталкивается с числом, выходящим за пределы диапазона 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 и NaN

static const int kInfinityOrNanExponent =          
(kExponentMask >> kExponentShift) - kExponentBias;


Проверка на Infinity и NaN важна, потому что эти значения имеют одинаковую структуру на уровне IEEE-754: у них устанавливаются все биты экспоненты (11 битов равны 1), но различаются значения мантиссы:
- Если экспонента максимальна, а мантисса = 0, то это ±Infinity.
- Если экспонента максимальна, а мантисса ≠ 0 — это NaN.

---

#V8 #JavaScript #HeapNumber #IEEE754 #JSразбор
🔥5