Как числа, дроби, строки и структуры лежат в памяти — и почему из-за этого 0,1 + 0,2 ≠ 0,3, счётчики уходят в минус, а "😀".length равно двум.
Листайте стрелками ← →, свайпом или кнопками внизу. В схемах нажимайте на биты и кнопки, двигайте ползунки.
Память хранит только биты. Восемь битов — байт, 256 значений. Двоичная запись длинная, поэтому её пишут в шестнадцатеричной: одна hex-цифра — ровно 4 бита, байт — ровно две цифры.
0xFF = 255 = 1111 1111, 0x100 = 256 — уже не влезает в байт.#6741d9, хеши, UUID, дампы xxd.Нажимайте на биты 32-битного числа или введите своё. Каждая четвёрка битов — одна hex-цифра.
У каждого байта есть номер — адрес. Переменная — это адрес и размер, указатель — 8-байтное число, равное адресу. Тип знает только компилятор: процессор видит байты и читает их так, как велит инструкция.
Выберите переменную или нажмите на любой байт и прочитайте байты как другой тип. Одни и те же байты дают совсем разные значения.
Беззнаковое число — просто сумма степеней двойки. Знаковое хранится в дополнительном коде: старший бит весит не +128, а −128. Поэтому −1 — это все единицы, а −x = ~x + 1.
Сложение одно и то же для обоих толкований — процессору всё равно, со знаком число или без. Значения ходят по кругу: за 255 идёт 0, за 127 — −128. Нажимайте на биты и кнопки.
int — 32 бита, Go int — 64 на 64-битных системах, C int — обычно 32, long — 32 на Windows и 64 на Linux. Python int не переполняется вовсе.signed — неопределённое поведение: компилятор вправе выкинуть вашу проверку.mid = (lo + hi) / 2 в двоичном поиске. На массиве больше 2³⁰ элементов сумма переполняется — ошибка жила в JDK девять лет.Выберите тип счётчика и скорость, с которой он растёт.
Что вернёт Java?
Math.abs(Integer.MIN_VALUE) // int, −2 147 483 648
−x = ~x + 1: 0x80000000 → 0x7FFFFFFF → 0x80000000. Получается то же отрицательное число. Так ломается Math.abs(hash) % n при выборе шарда — индекс выходит отрицательным. Безопасно: Math.floorMod(hash, n) или Math.absExact, который бросит исключение.Число с плавающей точкой — это двоичная научная запись: знак, порядок и мантисса. double (Python float, JS number, Go float64) — 1 + 11 + 52 бита, около 16 значащих десятичных цифр.
0,1 в двоичной системе — бесконечная дробь, как ⅓ в десятичной, поэтому хранится ближайшее представимое число. Чем больше число, тем больше шаг между соседними значениями. Введите число или выберите пример.
Каждая операция с плавающей точкой округляет результат. Ошибки маленькие, но копятся, а сравнение на равенство ломается сразу: 0.1 + 0.2 == 0.3 — false.
Decimal, Java BigDecimal, SQL numeric.math.isclose(a, b).Двигайте ползунок: сумма прибавляется N раз в разных представлениях.
Целые в number точны только до 2⁵³ = 9 007 199 254 740 992. Дальше шаг между соседними значениями становится 2, 4, 8… и нечётные числа просто пропадают.
int64-идентификатор в JSON, JSON.parse в браузере молча округляет его — и запрос уходит за чужой записью. Поэтому Twitter отдаёт id_str.BigInt.(2**32 | 0) === 0.
Number.MAX_SAFE_INTEGER // 9007199254740991
2 ** 53 + 1 // 9007199254740992
JSON.parse('{"id": 1234567890123456789}').id
// 1234567890123456800
BigInt("1234567890123456789") + 1n
// 1234567890123456790n
0.1 + 0.2 // 0.30000000000000004
(1.005).toFixed(2) // "1.00"
// 1.005 хранится как 1.00499999999999989…
Счётчик событий во float32 (так бывает в метриках, шейдерах и ML-коде). Что он покажет после 20 миллионов событий?
float n = 0;
for (int i = 0; i < 20000000; i++) n += 1;
printf("%.0f\n", n);
float32 24 бита мантиссы. После 2²⁴ = 16 777 216 шаг между соседними числами равен 2, и 16 777 216 + 1 округляется обратно до 16 777 216. Счётчик замирает навсегда без всяких ошибок. В float64 то же случится на 2⁵³ — реже, но тоже бывает.Многобайтное число можно положить в память старшим байтом вперёд (big-endian) или младшим (little-endian). x86 и ARM — little-endian. Сетевые протоколы и многие форматы файлов — big-endian, «сетевой порядок».
struct.pack('<I', n) # little-endian
struct.pack('>I', n) # big-endian (сеть)
В Go — binary.LittleEndian/BigEndian, в Java ByteBuffer по умолчанию big-endian, а JS DataView без третьего аргумента тоже читает big-endian. Переключите порядок и прочитайте байты «не тем» способом.
Процессор читает 8-байтное число быстрее и атомарно, если его адрес кратен 8. Поэтому компилятор вставляет между полями пустые байты, а размер структуры округляет до самого строгого выравнивания.
Go и C раскладывают поля в порядке объявления — порядок можно оптимизировать (линтер fieldalignment). JVM и Rust переставляют поля сами. Двигайте поля стрелками.
Go []int64, Java long[], NumPy — значения лежат подряд. Python list и Java Integer[] — массив указателей на отдельные объекты в куче, у каждого свой заголовок.
Процессор читает память строками кэша по 64 байта. Подряд — одна строка даёт 8 чисел, а предвыборка грузит следующие заранее. Через указатели — лишние строки и зависимые загрузки, особенно когда объекты разбросаны. Пройдите по массиву в каждом режиме. Подробнее про кэши — в презентации о процессоре.
В CPython всё — объект PyObject: счётчик ссылок, указатель на тип и данные. Целое — это массив «цифр» по 30 бит, поэтому оно не переполняется, но даже 0 занимает 28 байт.
257 is 257 может оказаться False.a + b — проверка типов и, скорее всего, новый объект. Отсюда сотни наносекунд на операцию против одной в Go.Введите число и посмотрите, из чего состоит объект.
Сколько памяти займёт в CPython список из миллиона разных целых?
xs = list(range(1_000_000))
sys.getsizeof скажет 28 байт, а аллокатор выдаёт блоки кратно 16 — 32 байта. Итого 8 + 32 = 40 МБ (tracemalloc покажет именно столько). array('q') или numpy.arange уложатся в 8 МБ и считаются в десятки раз быстрее.Unicode нумерует символы: «Я» — U+042F, «😀» — U+1F600, всего до 1,1 млн кодовых точек. UTF-8 кодирует номер от 1 до 4 байтами: ASCII — один байт, кириллица — два, эмодзи — четыре.
0…, 110…, 1110…, 11110…), остальные начинаются с 10. По любому байту видно, где начало символа.Наберите текст или выберите пример. Нажмите на символ, чтобы увидеть его биты.
"привет""😀"len(s), Rustбайты UTF-8124.length, Java, C#единицы UTF-1662len(s)кодовые точки61JS и Java хранят строки в UTF-16: символы за пределами U+FFFF — пара суррогатов. Обрезка «по длине» рвёт символы, лимит VARCHAR(n) бывает в символах, а в байтах — у Redis и Kafka. MySQL utf8 — это 3 байта и без эмодзи, нужен utf8mb4. Двигайте ползунок обрезки.
Ограничиваем имя пользователя 10 символами. Что выведет Go?
name := "Владислав"
if len(name) > 10 {
name = name[:10] + "…"
}
fmt.Println(name)
len в Go — байты: «Владислав» занимает 18 байт, а 10 байт — ровно 5 кириллических букв. Повезло, что граница чётная: name[:9] разрезал бы букву пополам, и получилось бы «Влад�». Считайте символы через utf8.RuneCountInString и режьте по []rune, а для эмодзи с модификаторами — по графемам.xxd, hexdump -C, od -An -tx1.Decimal(0.1) и (0.1).hex(), в JS x.toPrecision(20).unsafe.Sizeof/Offsetof, C pahole, Java JOL.sys.getsizeof (только сам объект), tracemalloc — реальные аллокации.lscpu | grep Endian, sys.byteorder.len(s.encode()), Buffer.byteLength(s), new TextEncoder().encode(s).length.
$ printf 'Hi, Мир 😀' | xxd -g1
00000000: 48 69 2c 20 d0 9c d0 b8 d1 80 20 f0 9f 98 80
>>> from decimal import Decimal
>>> print(Decimal(0.1))
0.1000000000000000055511151231257827021181583404541015625
>>> (0.1).hex()
'0x1.999999999999ap-4'
$ java -jar jol-cli.jar internals java.lang.Integer
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark)
8 4 (object header: class)
12 4 int Integer.value
Instance size: 16 bytes
float32 — 7). Не сравнивайте на равенство и не храните во float деньги. В JS целые точны до 2⁵³.Компьютер не знает, что такое число. Он знает только байты и инструкцию, которая их читает.