>>
Как Rust умудряется сделать Option> бесплатным?
Отличный вопрос! Вы абсолютно правы в своих догадках. Rust действительно использует оптимизацию представления нулевого указателя (null pointer optimization, NPO) . Это одна из ключевых фич, которая позволяет Option не иметь накладных расходов в большинстве практических случаев.
Суть оптимизации: если тип T имеет недопустимое битовое представление (например, нулевой указатель для Box, &T, &mut T, NonNull), то компилятор использует это значение для представления None. Дискриминант (флаг) не хранится отдельно — он вшит в само значение.
Вот как это работает для вашего примера с Box:
// Box - это просто указатель на кучу.
// По определению, Box никогда не может быть нулевым (null).
// Поэтому:
// - Some(box_val) -> любой ненулевой указатель
// - None -> нулевой указатель (0x0)
// Размер: 8 байт (как обычный указатель на 64-битной системе)
println!("{}", size_of::>>()); // 8
То же самое работает для ссылок и умных указателей:
let x: Option<&i32> = None; // занимает 8 байт (как и &i32)
let y: Option> = None; // занимает 8 байт
let z: Option> = None; // занимает 8 байт
Когда Option занимает больше места?
Оптимизация работает только для типов, у которых есть "дырка" в представлении . Для большинства простых типов (i32, bool, struct из полей) все битовые комбинации валидны, поэтому дискриминант добавляется явно:
// i32 может быть любым 32-битным числом — нет недопустимых значений
println!("{}", size_of::>()); // 8 (4 байта i32 + 4 байта дискриминанта)
println!("{}", size_of::()); // 4
// bool тоже не имеет "дырок" (0 и 1 — оба валидны)
println!("{}", size_of::>()); // 2 (1 байт bool + 1 байт выравнивания/дискриминанта)
println!("{}", size_of::()); // 1
Проверка на практике
Вы можете убедиться в этом, используя std::mem::size_of и std::mem::align_of:
use std::mem;
// Сырые указатели тоже имеют нулевую дырку
println!("Option<*const i32>: {}", mem::size_of::>()); // 8 (оптимизировано!)
println!("*const i32: {}", mem::size_of::<*const i32>()); // 8
// А вот Option уже не оптимизируется
println!("Option: {}", mem::size_of::>()); // 2
println!("u8: {}", mem::size_of::()); // 1
Производительность во время выполнения
Накладные расходы на проверку Option в рантайме минимальны:
Для оптимизированных типов (Box, ссылки): проверка — это просто сравнение указателя с нулём. Одна инструкция процессора.
Для неоптимизированных типов (i32): проверка — это сравнение дискриминанта с константой. Тоже одна инструкция.
В обоих случаях ветвление (if/else) предсказывается процессором, и современные CPU справляются с этим за 1–2 такта.
Более того, Rust часто устраняет проверки во время компиляции с помощью анализа потока данных (NLL — Non-Lexical Lifetimes). Если компилятор может доказать, что Option гарантированно Some, он просто уберёт проверку.
Итог
Option — это абстракция с нулевой стоимостью (zero-cost abstraction) для всех типов, которые имеют недопустимое представление (указатели, ссылки, NonNull, NonZero*). Для остальных типов накладные расходы составляют 1–8 байт на дискриминант, но проверка занимает одну инструкцию CPU. В 99% случаев это быстрее и безопаснее, чем ручная проверка на null в C++.
<<>>
Альтернативный взгляд: когда Option всё-таки может быть дорогим
Хотя в большинстве случаев Option бесплатен, есть ситуации, где оптимизация не работает и накладные расходы становятся заметными:
// 1. Option в массивах примитивов
// Массив из 1000 Option занимает 8000 байт (вместо 4000 для i32)
let arr: [Option; 1000] = [None; 1000];
println!("{}", mem::size_of_val(&arr)); // 8000
// 2. Option для пользовательских типов без "дырок"
struct MyType(u8, u8); // все 65536 комбинаций валидны
println!("{}", mem::size_of::>()); // 3 (2 байта данных + 1 байт дискриминанта)
// 3. Option в контейнерах с большим количеством вложенности
struct Deep {
a: Option >> // 3 уровня - размер 8 байт (только один дискриминант)
}
Рекомендация: Если вы храните миллионы маленьких значений в массиве и каждый байт на счету, рассмотрите альтернативы:
Используйте Vec> — он компактнее массива фиксированного размера.
Для целых чисел — NonZeroI32 (если 0 недопустим) — тогда Option будет размером с обычный i32.
Для пользовательских типов — добавьте "дырку" вручную, используя #[repr(transparent)] с NonNull.
Но в 99% случаев просто используйте Option — компилятор сделает всё за вас оптимально.