Раскладка памяти в 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, остальные разбираются в тренажёре.
- Что делает transmute и почему его называют последним средством?A)Копирует значение с преобразованием формата, как as для чиселB)Переинтерпретирует биты как другой тип, без проверокC)Переносит значение в другую область памяти без копированияD)Меняет тип переменной, оставляя проверки компилятора включёнными
показать ответ и разбор
+B)Переинтерпретирует биты как другой тип, без проверок// разбор: transmute берёт байты одного типа и объявляет их другим. Размеры обязаны совпадать — это компилятор проверит, а вот валидность результата нет: bool из байта 2 или ссылка из произвольного адреса дают UB. Почти всегда есть точный инструмент — from_bits у чисел, to_le_bytes, as для указателей, TryFrom для сужения.
- Почему size_of::<&[u8]>() равен 16, а size_of::<&u8>() — 8?A)Ссылка на срез толстая: адрес плюс длинаB)Компилятор выравнивает срезы по 16 байтC)Срез хранит ёмкость буфера дополнительно к адресуD)Ссылка на срез включает счётчик заимствований
показать ответ и разбор
+A)Ссылка на срез толстая: адрес плюс длина// разбор: Размер среза неизвестен на этапе компиляции, поэтому ссылка на него несёт длину рядом с адресом — это толстый указатель. Так же устроены &str и &dyn Trait, только у последнего вместо длины адрес таблицы методов. Ссылка на обычное значение остаётся одним словом.
- Где хранятся данные Vec<T> и что лежит в самой переменной?A)Данные и служебные поля целиком на стекеB)Данные в куче, в переменной — указатель, длина и ёмкостьC)Данные в куче, а длина хранится перед первым элементомD)Данные в куче, в переменной — только указатель на заголовок
показать ответ и разбор
+B)Данные в куче, в переменной — указатель, длина и ёмкость// разбор: Сам Vec — это три слова: адрес буфера, число элементов и ёмкость. Элементы лежат непрерывно в куче. Отсюда и цена перемещения — копируются 24 байта, а не содержимое, — и поведение при росте: буфер перевыделяется, а заголовок обновляется на месте.
- Сколько занимает структура без полей и что это даёт?A)Один байт, чтобы у каждого значения был собственный уникальный адрес в памятиB)Размер указателя, потому что значение всё равно размещается через ссылкуC)Размер выравнивания платформы — обычно восемь байт на 64-битной системеD)Ноль байтов: коллекция из миллиона таких значений не занимает места под элементы
показать ответ и разбор
+D)Ноль байтов: коллекция из миллиона таких значений не занимает места под элементы// разбор: Типы нулевого размера — не курьёз, а рабочий инструмент: HashSet<T> это HashMap<T, ()>, и значения в нём не занимают ничего. Vec из миллиона ZST хранит только счётчик, аллокации нет. На них же построены маркеры типов и PhantomData. Выравнивание при этом равно единице, а адрес у таких значений формально есть, просто он ничего не адресует.
- Почему структура { a: u8, b: u32 } с #[repr(packed)] занимает 5 байт, а её выравнивание равно 1?A)Потому что packed сжимает поля алгоритмом упаковки при записи в памятьB)Потому что packed переносит поля в кучу и оставляет в структуре только указательC)Потому что u32 в упакованной структуре хранится в укороченном виде без старших байтовD)Потому что packed убирает промежутки между полями и снимает требование к выравниванию
показать ответ и разбор
+D)Потому что packed убирает промежутки между полями и снимает требование к выравниванию// разбор: Атрибут запрещает компилятору вставлять выравнивающие байты: поля лежат вплотную, размер получается 1 + 4, а выравнивание всей структуры падает до единицы. Замер это подтверждает. Плата известна: поле u32 оказывается по невыровненному адресу, ссылку на него взять нельзя, а чтение идёт копированием значения. Берут packed для разбора бинарных форматов и обмена с железом.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.