Почему Rust не оптимизирует код, предполагая отсутствие алиасинга двух изменяемых ссылок?
Краткий ответ: LLVM не доверяет гарантиям Rust "из коробки"
Вы правы: Rust гарантирует, что две &mut T ссылки не могут алиасить (указывать на одну память). Однако компилятор Rust (rustc) не передаёт эту информацию напрямую в LLVM в виде атрибута noalias для всех случаев. Причина — исторические проблемы с корректностью LLVM и сложность с вложенными ссылками.
Почему так происходит?
В C/C++ ключевое слово restrict — это обещание программиста, которое LLVM использует для агрессивных оптимизаций. Если программист нарушает обещание — это UB. В Rust же гарантии даются компилятором, но LLVM иногда генерирует некорректный код, если слепо верить noalias для &mut T. Проблемы возникают, когда:
- Ссылка используется для записи в память, которая доступна через другую ссылку (например, через сырой указатель).
- Есть вложенные структуры с внутренней изменчивостью (
Cell,RefCell). - Функция принимает
&mut &mut T(двойная мутабельная ссылка).
Долгое время LLVM имел баги, связанные с noalias на noalias (пересекающиеся гарантии). Поэтому разработчики Rust пошли на компромисс: для большинства простых функций noalias не ставится, чтобы избежать случайных некорректных оптимизаций.
Как это выглядит на практике?
Ваш пример:
pub fn add_assign(x: &mut u32, y: &mut u32) {
*x += *y;
*x += *y;
}
Компилятор не может оптимизировать до *x += *y * 2, потому что x и y могут указывать на одну и ту же память с точки зрения LLVM. Rust не передал атрибут noalias для параметров.
Как заставить компилятор оптимизировать?
Есть несколько способов:
- Использовать
core::hint::assert_unchecked(ночной Rust):
#![feature(hint_assert_unchecked)]
use core::hint;
pub fn add_assign_opt(x: &mut u32, y: &mut u32) {
unsafe {
hint::assert_unchecked(!core::ptr::eq(x, y));
}
*x += *y;
*x += *y;
}
- Переписать через
NonNullи сырые указатели (для продвинутых случаев). - Использовать
#[repr(transparent)]и обёртки — иногда помогает структурирование кода. - Просто подождать — в Rust nightly уже есть эксперименты с включением
noaliasдля&mut Tв некоторых случаях (флаг-Zmutable-noalias).
Проверка на практике
Соберите с флагом -Zmutable-noalias (ночной Rust):
rustc -C opt-level=3 -Z mutable-noalias example.rs --emit asm
Вы увидите, что LLVM теперь генерирует lea eax, [rsi + rsi] или аналогичную оптимизацию. Но будьте осторожны: этот флаг может сломать код с unsafe, который полагается на алиасинг.
Вывод
Rust даёт гарантии, но LLVM пока не полностью им доверяет. Это компромисс между безопасностью и производительностью. Для большинства кода разница незначительна, но в горячих циклах можно использовать assert_unchecked или дождаться стабилизации -Zmutable-noalias. Система владения Rust всё равно даёт огромный выигрыш в производительности по сравнению с GC-языками, даже без этой конкретной оптимизации.
Альтернативный взгляд: это не только проблема LLVM, но и семантики Rust
Проблема глубже, чем кажется. Гарантия "две &mut T не алиассят" верна только для активных заимствований. Но Rust разрешает "мертвые" заимствования, которые уже не используются, но формально существуют. Рассмотрим пример:
fn foo(x: &mut u32, y: &mut u32) {
let a = x as *mut u32;
let b = y as *mut u32;
// Теперь x и y не используются, но ссылки живы
unsafe { *a += *b; }
}
Здесь x и y не алиассят, но мы создали сырые указатели, которые могут алиасить. Если LLVM решит, что x и y — noalias, и оптимизирует запись через a, это может привести к UB, которого Rust не обещал.
Поэтому Rust предпочитает консервативный подход: не ставить noalias, если есть хоть малейшая возможность обойти гарантию через unsafe. Это жертвует микрооптимизациями ради сохранения safety net для unsafe кода. Если вам нужна максимальная производительность — используйте #[repr(C)], сырые указатели и явные аннотации readonly/noalias через атрибуты LLVM (ночной Rust).
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги