ПрограммированиеСистемный Rust разработчик

Как устроено константное вычисление (const evaluation) в Rust? Чем const отличается от static и let, в каких случаях можно (и нужно) использовать const fn, и каковы ограничения при написании вычислений на этапе компиляции?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

Константное вычисление в Rust позволяет выполнять часть вычислений или инициализацию на этапе компиляции, а не во время выполнения программы.

  • const объявляет неизменяемую константу, вычисляемую на этапе компиляции, и не имеющую адреса в памяти:
const PI: f64 = 3.1415;
  • static объявляет глобальную переменную, расположенную в определённом сегменте памяти (обычно .data или .bss), возможна мутабельность (требует unsafe):
static mut GLOBAL_COUNTER: i32 = 0;
  • let используется для переменных на стеке, их значение может вычисляться в runtime, и они обязаны инициализироваться до первого использования.

const fn — это функция, результат которой может быть использован для задания значения const или static. Такие функции могут вызываться в константном контексте.

const fn factorial(n: usize) -> usize { if n == 0 { 1 } else { n * factorial(n - 1) } } const FACT_5: usize = factorial(5); // Компилируется!

Ограничения const fn:

  • Можно использовать только другие const fn,
  • Нельзя использовать heap-выделение (Box::new и прочее),
  • Нельзя вызывать небезопасные операции,
  • Нет доступа к внешним переменным и функциям (если они не const fn).

Вопрос с подвохом.

Вопрос: Можно ли использовать любую функцию в const-контексте, если её результат не изменяется? Например, вот так:

fn add(a: i32, b: i32) -> i32 { a + b } const RES: i32 = add(1, 2);

Типичный неверный ответ: Да, ведь функция чистая и результат известен заранее.

Правильный ответ: Нет, функция должна быть объявлена явно как const fn, только тогда её можно вызвать внутри const-инициализации. Обычные функции вызываются только на этапе выполнения!

Пример:

const fn add(a: i32, b: i32) -> i32 { a + b } const RES: i32 = add(1, 2); // Компилируется!

Примеры реальных ошибок из-за незнания тонкостей темы.


История

В проекте с вычислениями трёхмерной геометрии разработчик пытался объявить таблицу значений через результат вызова обычной функции, а не const fn. В результате появились ошибки компиляции, и был утрачен профит от compile-time вычисления.


История

Использование static mut для глобального кеша привело к гонке данных при обращении из нескольких потоков (static mut не безопасен!). Нужно было использовать Atomic или Mutex, чтобы синхронизировать доступ к глобальному ресурсу.


История

В попытке ускорить инициализацию больших массивов определяли их как static, но забыли, что static всегда имеет фиксированный адрес, из-за чего данные не попадали в кеш процессора как локальные, а операции замедлились на горячем пути логики сервера. Нужно было использовать локальные let-выражения с вычислениями в runtime.