В Rust вышел fearless_simd 0.5: безопасный SIMD с записью a + b

Автор технического блога объясняет, как сделать высокоуровневые SIMD-абстракции в Rust по-настоящему безопасными. Исходная проблема: сырые SIMD-интринсики неудобны (вместо a + b приходится писать громоздкий код с cfg-условиями под AVX2, SSE и NEON, и всё это внутри unsafe), а существующие абстракции прячут unsafe внутри себя, и там, по словам автора, скрыты большие объёмы unsafe, в которых время от времени случаются ошибки.

Почему вообще нужен unsafe: вызывать интринсик нельзя, если нужной инструкции нет на процессоре, поэтому раньше приходилось вручную или генератором кода отслеживать, какие инструкции нужны каждому интринсику. Начиная с Rust 1.87 компилятор сам отслеживает требуемые наборы инструкций, и внутри функции с правильным #[target_feature] интринсики можно вызывать без unsafe. Но unsafe всё равно остаётся где-то в цепочке вызовов, а повесить #[target_feature] на реализацию оператора + нельзя, потому что + должен быть доступен всегда.

Решение складывается из трёх частей. Во-первых, токен возможности процессора (CPU feature token): тип нулевого размера с приватным полем, который можно получить только из функции, выполнившей проверку наличия инструкций; токен существует лишь как доказательство на этапе компиляции и не стоит ничего во время работы. Во-вторых, макрос, безопасный для вызова: он оборачивает функцию с #[target_feature] во внешнюю обычную функцию, принимающую токен, и содержит единственный unsafe-блок, скрытый за корректным API, как у println!. Проверять и аудитировать нужно только этот макрос; ошибиться в нём можно лишь двумя способами (сопоставить токен не с тем #[target_feature] или позволить вызвать unsafe fn из безопасного контекста), и оба легко проверить. В-третьих, обобщённые типы вроде f32x8 с параметром уровня набора инструкций: токен прячется внутри значения, а для каждого уровня (Avx2, NoSimd и т. д.) пишется своя реализация сложения, так что a + b работает без явной передачи токенов и сам выбирает лучшие инструкции.

Ограничение, которое автор называет фундаментальным: при вызове a + b на SIMD-типах функция-вызывающая должна иметь #[inline(always)] или #[target_feature]. Иначе код компилируется, но компилятор не может встроить функцию с #[target_feature] в вызывающую без этой аннотации, появляются накладные расходы вызова, и производительность резко падает. RFC Struct Target Features, по словам автора, решает проблему для add_avx2(token, a, b), но пути к a + b он пока не видит.

Итог: рабочая версия идей доступна в fearless_simd v0.5. Макрос открыт публично, так что высокоуровневые операции вроде a + b можно сочетать с платформенными интринсиками. В fearless_simd больше одного unsafe-блока, потому что крейт заодно предоставляет функциональность safe_unaligned_simd, но и там unsafe, по утверждению автора, значительно меньше, чем в оригинале. Автор отмечает, что, насколько ему известно, он первым внедрил это в продакшен, хотя идеи старые: токены возможностей давно использует крейт pulp, но с вручную написанными небезопасными обёртками над интринсиками, где иногда допускались ошибки.

Ключевые факты

  • С Rust 1.87 компилятор сам отслеживает наборы инструкций для интринсиков, но unsafe всё равно остаётся где-то в цепочке вызовов, а на реализацию + повесить #[target_feature] нельзя.
  • Токен возможности процессора, тип нулевого размера с приватным полем, получить который можно только после проверки наличия инструкций; на время работы он не влияет.
  • Один безопасный для вызова макрос оборачивает функции с #[target_feature] в единственный unsafe-блок; аудировать нужно только его, а не сотни отдельных unsafe-блоков.
  • Обобщённые типы вроде f32x8 с параметром уровня прячут токен внутри и дают отдельную реализацию + для каждого набора инструкций, в том числе без SIMD.
  • Ограничение: a + b на SIMD-типах нужно вызывать из функции с #[inline(always)] или #[target_feature], иначе код компилируется, но производительность падает.

Почему это важно

SIMD нужен ради скорости, но высокоуровневые абстракции в Rust раньше прятали внутри много unsafe, где, по словам автора, время от времени случались ошибки. Здесь предложен способ, при котором весь unsafe сосредоточен в одном макросе, который можно проверить целиком, а пользователь пишет обычное a + b. Автор пишет, что для него именно объём unsafe был барьером для использования высокоуровневых SIMD-абстракций.

Кому это важно

Разработчикам на Rust, которые пишут вычислительно тяжёлый код (обработка массивов чисел, графика, кодеки), и авторам крейтов, которым нужны SIMD-реализации под несколько платформ с выбором лучшей во время выполнения. Также тем, кого останавливало обилие unsafe в SIMD-библиотеках.

Как это применить

Можно использовать крейт fearless_simd v0.5, в котором реализованы описанные идеи. Публично открытый макрос позволяет сочетать операции вроде a + b с платформенными интринсиками. Практическое правило из статьи: вызывать a + b на SIMD-типах из функции с #[inline(always)] или #[target_feature], иначе производительность упадёт. Идею можно перенять и в своём коде: токен проверки возможностей, безопасная обёртка в макросе и обобщённые типы по уровню набора инструкций.

Можно ли доверять

Это авторский технический текст, а не независимая оценка: утверждения вроде «беспрецедентно низкий уровень unsafe» и «первым внедрил в продакшен» принадлежат автору. Числовых замеров производительности fearless_simd в тексте нет. Код-примеры упрощены (например, тип f32x8 в примере для простоты построен на массиве, а в продакшене используются нативные SIMD-типы), а версия 0.5, рабочая реализация идей, а не их упрощённая демонстрация.

Риски и подводные камни

Главная ловушка, встраивание: если вызвать a + b из функции без #[inline(always)] или #[target_feature], код всё равно скомпилируется, но станет заметно медленнее. Автор не видит пути к решению этой проблемы для a + b даже с RFC Struct Target Features и считает ограничение фундаментальным для любого подхода к a + b на SIMD. Кроме того, unsafe не исчезает полностью: в макросе остаётся один unsafe-блок, а в fearless_simd их больше одного из-за функциональности safe_unaligned_simd. Корректность обеспечивается тем, что макрос правильно сопоставляет токен с #[target_feature] и не допускает вызова unsafe fn из безопасного контекста.

«Но теперь SIMD в Rust может быть по-настоящему бесстрашным.»

— автор статьи о fearless_simd