Avatar ·

Когда и зачем использовать атрибут `#[inline]` в Rust? Нужен ли он вообще?

Всем привет! Я новичок в Rust и пытаюсь разобраться с оптимизациями. Читал, что в C/C++ ключевое слово `inline` может ускорить код, заменив вызов функции её телом. В Rust тоже есть атрибут `#[inline]`, но я не до конца понимаю, когда его применять. Написал простую функцию для сложения двух чисел: ```rust fn add(a: i32, b: i32) -> i32 { a + b } ``` И другую, которая вызывает её в цикле: ```rust fn sum_squares(n: i32) -> i32 { let mut sum = 0; for i in 0..n { sum += add(i, i); } sum } ``` Если я добавлю `#[inline(always)]` к `add`, будет ли это иметь смысл? Компилятор сам не догадается? Или, наоборот, `#[inline(never)]` может быть полезен для уменьшения размера бинарника? Видел, что в стандартной библиотеке Rust некоторые функции помечены `#[inline]`, но не все. Значит, это нужно делать осознанно. Как разработчику решить, стоит ли вставлять `#[inline]` в свой код? Есть ли какие-то эмпирические правила или это всегда преждевременная оптимизация? В общем, хотелось бы понять философию использования `inline` в Rust: когда это помогает, когда вредит, и стоит ли вообще об этом беспокоиться новичку. Заранее спасибо за разъяснения! <<>>

Краткий ответ: доверяйте компилятору, но знайте, когда вмешаться

Отличный вопрос! В Rust (и в LLVM, на котором он основан) компилятор уже выполняет инлайнинг автоматически на основе эвристик. #[inline] — это не команда, а подсказка компилятору. В большинстве случаев новичкам не нужно его использовать. Однако есть конкретные сценарии, когда это оправдано.

Как работает инлайнинг в Rust?

Компилятор Rust (rustc) использует LLVM для генерации машинного кода. LLVM сам решает, какие функции стоит инлайнить, исходя из:

  • Размера функции (маленькие функции инлайнятся чаще)
  • Частоты вызовов (внутри горячих циклов)
  • Атрибутов inline

Ваш пример с add — классический случай, когда компилятор сам всё сделает. Функция настолько мала, что LLVM инлайнит её без всяких подсказок. Проверьте это через cargo rustc -- --emit asm — в ассемблерном коде не будет вызова add.

Когда использовать #[inline]?

Есть три варианта атрибута:

  • #[inline] — "пожалуйста, подумай об инлайне" (самый мягкий)
  • #[inline(always)] — "обязательно инлайнь" (агрессивно)
  • #[inline(never)] — "никогда не инлайнь" (запрет)

Основные случаи для #[inline]:

  1. Кросс-крейтовый инлайнинг. По умолчанию функции из других крейтов не инлайнятся. Если вы пишете библиотеку и хотите, чтобы маленькая функция (например, геттер) была инлайнена в крейте пользователя — пометьте её #[inline].
  2. Функции-обёртки. Если функция просто вызывает другую функцию, инлайн может убрать накладные расходы.
  3. Горячие циклы. Если функция вызывается миллионы раз внутри цикла, и вы уверены, что инлайн ускорит код (измерьте бенчмарками!).

Когда #[inline(always)] может навредить?

Агрессивный инлайнинг увеличивает размер бинарника (code bloat). Это может:

  • Замедлить кэш инструкций процессора
  • Увеличить время компиляции
  • В редких случаях — ухудшить производительность из-за промахов кэша

Поэтому #[inline(always)] стоит применять только для очень маленьких функций (до 10-15 строк) и только после профилирования.

Пример из реальной практики

Допустим, вы пишете библиотеку для работы с векторами:

pub struct Vec3 {
    x: f64,
    y: f64,
    z: f64,
}

impl Vec3 {
    // Эта функция будет вызываться из другого крейта
    #[inline]
    pub fn dot(&self, other: &Vec3) -> f64 {
        self.x * other.x + self.y * other.y + self.z * other.z
    }
}

Без #[inline] каждый вызов dot из пользовательского кода будет реальным вызовом функции. С атрибутом — компилятор пользователя сможет встроить код прямо в место вызова, что критично для высокопроизводительных вычислений.

Золотое правило

Не используйте #[inline] без профилирования. Начните с написания чистого кода. Если заметили узкое место — измерьте с помощью perf или cargo bench. И только потом, если профилировщик показывает, что вызов функции занимает значительное время, попробуйте добавить #[inline].

Для вашего примера с add — не нужно ничего делать. Компилятор сам оптимизирует этот код до идеала.

<<>>

Альтернативный взгляд: инлайнинг и generic-функции

Есть ещё один важный аспект, который стоит упомянуть — это работа с generic-функциями и трейтами. В Rust дженерики мономорфизируются, то есть для каждого конкретного типа создаётся отдельная версия функции. Это автоматически даёт инлайнинг, если функция маленькая.

Однако есть нюанс с динамической диспетчеризацией (через dyn Trait). Если вы используете трейт-объекты, компилятор не может заранее знать конкретный тип, и инлайнинг становится невозможным. В таких случаях можно:

  • Переписать код на статическую диспетчеризацию (generic-параметры)
  • Или смириться с небольшими накладными расходами

Также стоит помнить, что #[inline] влияет на размер кода при мономорфизации. Если у вас generic-функция, которая инстанцируется для 10 разных типов, и вы пометили её #[inline(always)], то каждый вариант будет встроен во все места вызова. Это может привести к взрывному росту бинарника.

Мой совет: для библиотек используйте #[inline] только для самых маленьких и часто используемых функций-аксессоров. Для всего остального — профилируйте. Новые версии компилятора Rust становятся умнее с каждым релизом, и ручные подсказки нужны всё реже.