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