сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · unsafe и производительность Rust

Раскладка памяти в Rust

Раскладка памяти

Блок для тех, кто работает с бинарными форматами и производительностью. Спрашивают про размеры, выравнивание, packed и transmute.

Стержень: раскладка по умолчанию - дело компилятора, и это свобода, которой он пользуется ради уплотнения.

// Формулировки: «сколько занимает эта структура?», «чем опасен packed?», «когда допустим transmute?»

Порядок полей и выравнивание

Каждый тип требует выравнивания: u32 хочет адрес, кратный четырём. Компилятор в обычной раскладке переставляет поля так, чтобы промежутков было меньше: { u8, u32, u8 } влезает в 8 байт. С repr(C) порядок фиксирован, поэтому появляются заполнители и получается 12 - проверено на компиляторе.

Из этого следует простое правило для repr(C)-структур: объявляй поля от больших к меньшим, тогда заполнителей будет меньше. Для обычной раскладки заботиться не нужно, компилятор сделает это сам.

// Толстые указатели - отдельная деталь: &[u8] и &str занимают 16 байт (адрес плюс длина), у &dyn Trait вторая половина - адрес vtable. Обычная ссылка остаётся одним словом.

выравнивание
требование к адресу, кратному размеру типа
заполнители
неиспользуемые байты между полями ради выравнивания

packed и transmute

repr(packed) убирает промежутки целиком: { u8, u32 } станет пятью байтами вместо восьми. Плата серьёзная - поля перестают быть выровненными, и ссылку на такое поле взять уже нельзя: только копировать значение. На части архитектур невыровненный доступ ещё и медленнее. Оправдано это при разборе бинарных форматов, где раскладка задана извне.

transmute берёт байты одного типа и объявляет их другим. Совпадение размеров компилятор проверит (иначе E0512), а валидность результата - нет: bool из байта 2 или ссылка из произвольного адреса дают UB (undefined behavior). Почти всегда есть точный инструмент: f32::from_bits вместо трансмута из u32, to_le_bytes для сериализации, TryFrom для сужения чисел.

// Проверено: transmute u32 в f32 работает и даёт ожидаемое число, а from_bits делает то же самое без единого unsafe.

repr(packed)
упаковка без промежутков ценой невыровненных полей
transmute
переинтерпретация битов как другого типа

Как отвечать: «Сколько занимает структура из u8, u32 и u8?»

Зависит от раскладки. В обычной, которая по умолчанию, компилятор вправе переставить поля и уложит их в 8 байт: сначала u32, потом два u8 и заполнитель. С repr(C) порядок объявления фиксирован, поэтому между первым u8 и u32 появляются три байта выравнивания, и в хвосте ещё три - получается 12. С repr(packed) промежутков не будет вовсе, шесть байт, но поля перестанут быть выровненными, и ссылку на них взять уже нельзя. Отсюда правило: если структура уезжает в C, объявляй поля от больших к меньшим.

Ты даёшь три числа под три раскладки и объясняешь механику это проверяемое знание, которое сразу видно по ответу.

На чём валятся

  • Рассчитывают на порядок полей без repr(C).
  • Берут ссылку на поле packed-структуры и получают ошибку компиляции.
  • Считают, что transmute проверяет валидность результата - он проверяет только размер.
  • Не знают про from_bits и делают через transmute то, что есть в безопасном виде.
  • Забывают, что &str и &[u8] - толстые указатели, и удивляются размеру структур.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_memory_layout1 / 5
    Что делает transmute и почему его называют последним средством?
    A)Копирует значение с преобразованием формата, как as для чисел
    B)Переинтерпретирует биты как другой тип, без проверок
    C)Переносит значение в другую область памяти без копирования
    D)Меняет тип переменной, оставляя проверки компилятора включёнными
    показать ответ и разбор
    +B)Переинтерпретирует биты как другой тип, без проверок

    // разбор: transmute берёт байты одного типа и объявляет их другим. Размеры обязаны совпадать — это компилятор проверит, а вот валидность результата нет: bool из байта 2 или ссылка из произвольного адреса дают UB. Почти всегда есть точный инструмент — from_bits у чисел, to_le_bytes, as для указателей, TryFrom для сужения.

  2. #rs_memory_layout2 / 5
    Почему size_of::<&[u8]>() равен 16, а size_of::<&u8>() — 8?
    A)Ссылка на срез толстая: адрес плюс длина
    B)Компилятор выравнивает срезы по 16 байт
    C)Срез хранит ёмкость буфера дополнительно к адресу
    D)Ссылка на срез включает счётчик заимствований
    показать ответ и разбор
    +A)Ссылка на срез толстая: адрес плюс длина

    // разбор: Размер среза неизвестен на этапе компиляции, поэтому ссылка на него несёт длину рядом с адресом — это толстый указатель. Так же устроены &str и &dyn Trait, только у последнего вместо длины адрес таблицы методов. Ссылка на обычное значение остаётся одним словом.

  3. #rs_memory_layout3 / 5
    Где хранятся данные Vec<T> и что лежит в самой переменной?
    A)Данные и служебные поля целиком на стеке
    B)Данные в куче, в переменной — указатель, длина и ёмкость
    C)Данные в куче, а длина хранится перед первым элементом
    D)Данные в куче, в переменной — только указатель на заголовок
    показать ответ и разбор
    +B)Данные в куче, в переменной — указатель, длина и ёмкость

    // разбор: Сам Vec — это три слова: адрес буфера, число элементов и ёмкость. Элементы лежат непрерывно в куче. Отсюда и цена перемещения — копируются 24 байта, а не содержимое, — и поведение при росте: буфер перевыделяется, а заголовок обновляется на месте.

  4. #rs_memory_layout4 / 5
    Сколько занимает структура без полей и что это даёт?
    A)Один байт, чтобы у каждого значения был собственный уникальный адрес в памяти
    B)Размер указателя, потому что значение всё равно размещается через ссылку
    C)Размер выравнивания платформы — обычно восемь байт на 64-битной системе
    D)Ноль байтов: коллекция из миллиона таких значений не занимает места под элементы
    показать ответ и разбор
    +D)Ноль байтов: коллекция из миллиона таких значений не занимает места под элементы

    // разбор: Типы нулевого размера — не курьёз, а рабочий инструмент: HashSet<T> это HashMap<T, ()>, и значения в нём не занимают ничего. Vec из миллиона ZST хранит только счётчик, аллокации нет. На них же построены маркеры типов и PhantomData. Выравнивание при этом равно единице, а адрес у таких значений формально есть, просто он ничего не адресует.

  5. #rs_memory_layout5 / 5
    Почему структура { a: u8, b: u32 } с #[repr(packed)] занимает 5 байт, а её выравнивание равно 1?
    A)Потому что packed сжимает поля алгоритмом упаковки при записи в память
    B)Потому что packed переносит поля в кучу и оставляет в структуре только указатель
    C)Потому что u32 в упакованной структуре хранится в укороченном виде без старших байтов
    D)Потому что packed убирает промежутки между полями и снимает требование к выравниванию
    показать ответ и разбор
    +D)Потому что packed убирает промежутки между полями и снимает требование к выравниванию

    // разбор: Атрибут запрещает компилятору вставлять выравнивающие байты: поля лежат вплотную, размер получается 1 + 4, а выравнивание всей структуры падает до единицы. Замер это подтверждает. Плата известна: поле u32 оказывается по невыровненному адресу, ссылку на него взять нельзя, а чтение идёт копированием значения. Берут packed для разбора бинарных форматов и обмена с железом.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.