Почему Rust не оптимизирует код, если две изменяемые ссылки (mutable references) не могут алиаситься?
Краткий ответ: Гарантии Rust есть, но LLVM им не доверяет без дополнительного флага
Вы абсолютно правы в своих рассуждениях. С точки зрения семантики Rust, две изменяемые ссылки действительно не могут алиаситься. Это одно из главных правил borrow checker'а. Однако проблема кроется не в Rust, а в том, как это транслируется в промежуточное представление (IR) для LLVM.
Дело в том, что LLVM — это бэкенд, который изначально заточен под C/C++. В C/C++ для указания на то, что указатели не пересекаются, используется ключевое слово restrict. Rust, со своей стороны, не генерирует атрибут noalias для аргументов &mut T по умолчанию (за некоторыми исключениями, о которых ниже).
Почему? Исторически сложилось так, что в ранних версиях Rust (до 1.0 и некоторое время после) включение noalias для всех &mut приводило к генерации неверного кода. LLVM иногда делал слишком агрессивные оптимизации, которые ломали код, особенно при работе с вложенными ссылками, типами с внутренней изменяемостью (например, UnsafeCell) или при передаче ссылок на аргументы, которые на самом деле являются алиасами на глобальное состояние.
Рассмотрим ваш пример:
pub fn add(x: &mut u32, y: &mut u32) -> u32 {
*x + *y
}
В LLVM IR эта функция выглядит примерно так (упрощенно):
define i32 @add(i32* %x, i32* %y) {
%val1 = load i32, i32* %x
%val2 = load i32, i32* %y
%sum = add i32 %val1, %val2
ret i32 %sum
}
Без атрибута noalias на указателях %x и %y, LLVM обязан выполнить обе загрузки из памяти. Почему? Потому что он не знает, не изменится ли значение *x после того, как мы прочитали *y (теоретически, они могут указывать на одно и то же место). Если бы атрибут noalias был, LLVM мог бы переставить операции местами или кэшировать значение, но без него — нет.
Как это исправить? Флаг -Zmutable-noalias
Начиная с Rust 1.70 (и особенно активно в ночных сборках), команда Rust работает над возвращением этой оптимизации. Теперь она доступна через флаг компилятора:
RUSTFLAGS="-Z mutable-noalias" cargo build --release
Этот флаг заставляет компилятор добавлять атрибут noalias к аргументам &mut T в LLVM IR. После его включения код вашей функции будет оптимизирован гораздо агрессивнее.
Однако, этот флаг пока считается экспериментальным. Он может сломать код, который использует:
UnsafeCell(основаCell,RefCell,Mutex) — так как LLVM может решить, что запись в ячейку через&mutне влияет на другие указатели, хотя на самом деле влияет через внутреннюю изменяемость.- FFI (Foreign Function Interface) — если вы передаете указатель на C-код, который меняет память "за спиной" у Rust,
noaliasможет привести к неправильным предположениям.
Почему не включено по умолчанию?
Rust очень консервативен в вопросах корректности. Включение noalias для всех случаев привело бы к ошибкам в легальном коде, использующем unsafe. Поэтому команда разработчиков сначала хочет убедиться, что MIR (Mid-level IR) может доказать безопасность noalias для каждого конкретного случая, а не просто верить на слово LLVM.
Таким образом, ваш вопрос упирается в разрыв между высокоуровневыми гарантиями Rust и низкоуровневыми возможностями LLVM. Rust знает, что ссылки не алиасятся, но пока не говорит об этом LLVM в полной мере, чтобы не сломать существующий код.
<<Дополнение: Проблема с типами, содержащими UnsafeCell
Ответ выше верен, но стоит подчеркнуть ключевую причину, по которой noalias опасен. Представьте, что у вас есть структура, содержащая UnsafeCell:
use std::cell::UnsafeCell;
struct Foo {
value: UnsafeCell<u32>,
}
fn example(x: &mut Foo, y: &mut Foo) {
// С точки зрения Rust, x и y не пересекаются.
// Но внутри UnsafeCell мы можем получить сырой указатель
// и сделать так, чтобы они указывали на одно и то же место.
let ptr1 = x.value.get();
let ptr2 = y.value.get();
// Если x и y — это один и тот же объект (что запрещено правилами &mut,
// но возможно в unsafe коде), то ptr1 и ptr2 равны.
unsafe {
*ptr1 = 5;
*ptr2 = 10; // Перезаписывает *ptr1
}
}
Если бы LLVM добавил noalias к аргументам x и y, он мог бы переупорядочить запись в *ptr1 и *ptr2, или вообще удалить одну из них, посчитав, что они не влияют друг на друга. Это привело бы к неопределенному поведению (UB).
Поэтому Rust не может просто так раздавать noalias всем подряд. Он должен убедиться, что внутри функции не происходит "небезопасного" обхода системы типов. Текущая реализация с флагом -Z mutable-noalias пытается это делать, анализируя тело функции на наличие UnsafeCell, но это сложная задача, которая пока не завершена.
Таким образом, "отсутствие" оптимизации — это цена за безопасность в присутствии unsafe кода. Как только анализатор MIR станет достаточно умным, чтобы доказать отсутствие UnsafeCell в цепочке заимствования, оптимизация будет включена по умолчанию.
Ответов пока нет.
Войдите чтобы оставить ответ