Вопрос или проблема
Меня удивило, что Option<Vec<T>>
имеет такой же размер, как Vec<T>
:
fn main() {
println!(
"u128: {} -> {}",
size_of::<u128>(),
size_of::<Option<u128>>()
);
println!(
"vec: {} -> {}",
size_of::<Vec<u16>>(),
size_of::<Option<Vec<u16>>>()
);
}
оценка равна
u128: 16 -> 32
vec: 24 -> 24
Так что
-
Как Rust представляет случай
None
дляOption<Vec>
? Похоже, что нормальные 24 байтаVec
приходят из трех 8-байтовых полей: указателя, емкости в байтах и длины. Я предполагаю, что он использует нулевой указатель дляNone
, но не уверен. Сможет ли Rust поддерживать такую же структуру памяти, еслиVec
будет содержать 2 указателя? -
Почему Rust это делает? Для
Option<u128>
у нас явно есть выбор использоватьN
байтов, покаN > 16
, а Rust выбираетN = 32
. Это, конечно, имеет свои преимущества, например, мы можем получить доступ кi
-му элементуVec<Option<u128>>
только с помощью битовых сдвигов (без умножений). Но если это идеально, разве Rust не должен сделатьOption<Vec<T>>
также 32 байта?
Версия Rust: rustc 1.83.0-nightly (1bc403daa 2024-10-11)
1. Option<Vec<_>>
Вы обнаружили нишевую оптимизацию использования памяти, поддерживаемую Vec
. Более конкретно:
Vec<T>
содержит…RawVec<T>
, который содержит…RawVecInner
, который содержит оба…Cap
,usize
, который никогда не может превышатьisize::MAX
(известноrustc
черезrustc_layout_scalar_valid_range_end
)Unique<u8>
, который содержит…NonNull<u8>
. Документация дляNonNull
специально отмечает, что отсутствие NULL позволяет оптимизировать структуру памяти.
В результате Option::<Vec<_>>::None
может быть представлено 0, где должен быть указатель (что отличается от действительной инстанции нулевого *const u8
указателя). В качестве альтернативы это может быть представлено значением, превышающим isize::MAX
, где должен быть Cap
. Остальные поля могут быть произвольными для такого значения. Компилятор получает возможность выбирать, какое представление использовать произвольно, но я бы предположил, что все нулевые байты будут оптимальными для производительности (легче распределять).
Я не уверен, что вы имеете в виду под Vec
, который содержит два указателя. Добавление дополнительного указателя в Vec
увеличило бы размер как Vec
, так и Option<Vec<_>>
на размер указателя.
2. Option<u128>
Сначала давайте предположим, что указатели занимают 8 байтов и выравнивание u128
составляет 16:
println!("{}", std::mem::size_of::<*const u8>()); // 8
println!("{}", std::mem::align_of::<u128>()); // 16
Рассмотрим значение vec![Some(0u128), Some(0u128)]
. Для корректности оба значения u128
должны быть выровнены. По вашей логике, первый элемент в Vec
должен занимать как минимум 17 байтов (16 для u128
, 1 для дискриминанта enum
). Следующее выровненное местоположение в памяти (множитель выравнивания) будет находиться на 32 байта от начала Vec
. Поскольку Vec
не имеет способа добавлять отступы между элементами, отступы должны попадать внутрь Option<u128>
, что делает его 32 байта. На самом деле, размер является кратным выравниванию для всех типов.
Если вам интересно, ближайшим к 16-байтовому Option<u128>
является Option<NonZeroU128>
.
Но если это идеально, разве Rust не должен сделать Option<Vec> также 32 байта?
Как я уже упоминал, это проблема выравнивания, а не проблема производительности индексации. Vec
имеет выравнивание 8, поэтому размер должен быть только кратным 8. Если вас беспокоит производительность индексации, всегда можно увеличить выравнивание:
/// Отказ от ответственности: фактическая производительность может варьироваться;
/// проконсультируйтесь с вашим бенчмарком перед принятием каких-либо решений об оптимизации!
///
/// Увеличение использования памяти на 33% снизит локальность кеша!
#[repr(align(32))]
struct FastVec<T>(Vec<T>);
Ответ или решение
Как и почему Rust оптимизирует Option<Vec<T>>
до 24 байтов?
Rust известен своей эффективной системой управления памятью и сосредоточенности на производительности. Одной из интересных особенностей этого языка является то, что размер Option<Vec<T>>
равен размеру самого Vec<T>
, что составляет 24 байта в большинстве современных архитектур. В этой статье мы рассмотрим, как достигается такая оптимизация, и обсудим причины ее реализации.
1. Как Rust представляет случай None
для Option<Vec>
?
Структура Vec<T>
в Rust состоит из трех 8-байтных полей:
- Указатель на данные в куче
- Емкость (capacity) — максимальное количество элементов, которое может храниться в
Vec
без повторного выделения памяти - Длина (length) — текущее количество элементов в
Vec
Когда мы используем Option<Vec<T>>
, может возникнуть случай None
. Для эффективного представления этого состояния Rust применяет трюк с указателями. В случае None
указатель просто устанавливается в 0
. Этот нулевой указатель недопустим для валидной памяти, и поскольку указатели не могут быть NULL
в структуре NonNull
, это дает возможность компилятору избежать необходимости выделять дополнительные 8 байт для хранения информации о стеке вызовов.
Таким образом, для Option<Vec<T>>
представление случая None
использует 0 в поле указателя. Другие поля могут иметь произвольные значения, но нулевой указатель применяется как индикатор отсутствия значения. Это позволяет Option<Vec<T>>
оставаться на уровне 24 байтов, в то время как в других случаях, таких как Option<u128>
, размер увеличивается в 32 байта.
2. Почему Rust использует данную оптимизацию?
Вопрос о том, почему Option<Vec<T>>
не занимает 32 байта, как, например, Option<u128>
, связан с учетом выравнивания:
u128
требует выравнивания в 16 байт. Это значит, что для его хранения необходима память, кратная 16. Следовательно, 17 байт (16 дляu128
и 1 для маркера) становятся недостаточными, и для сохранения правильного выравнивания придется использовать 32 байта.Vec
же, с другой стороны, требует только выравнивания в 8 байт. В этом случае, 24 байта вполне достаточно для хранения указателя, емкости и длины, что означает, что Rust по умолчанию может использовать 24 байта дляOption<Vec<T>>
.
Этот механизм позволяет Rust минимизировать расходы по памяти и повысить производительность ссылок и операций со структурами данных, не увеличивая объем используемой памяти без необходимости.
Заключение
Таким образом, оптимизация Option<Vec<T>>
до 24 байтов в Rust осуществляется благодаря умной системе представления указателей и управлению памятью. В случае None
указатель представляет собой нулевое значение, что позволяет удерживать общий размер структуры на уровне 24 байтов. Это решение является частью философии Rust о том, чтобы сохранять производительность и управляемость памяти, одновременно обеспечивая безопасность типов. Подобная оптимизация высоко оценивается программистами, работающими с производительными программными обеспечениями и играми, где каждая байта памяти имеет значение.