🏎️ SIMD для ленивых: Автовекторизация в Rust
SIMD - это когда процессор одной инструкцией складывает не два числа, а сразу 4, 8 или 16.
Многие думают, что SIMD это сложно и требует
Допустим, у вас есть цикл:
Если вы скомпилируете это в релизе для x86_64, компилятор сгенерирует инструкции SSE2 (обрабатывая по 4 числа
Как сделать еще быстрее?
Если вы знаете, что ваш код будет работать на современном железе, разрешите компилятору использовать AVX2 (по 8 чисел
Но есть нюанс: Компилятор очень осторожен. Он не применит SIMD, если:
1. Есть риск паники. Если срезы разной длины, компилятор вставит проверки границ (
• Лечение: Используйте итераторы (
2. Есть риск алиасинга. Если
• Лечение: Rust тут молодец, его правила заимствования гарантируют отсутствие мутабельного алиасинга.
Всегда смотрите в ассемблер (
#rust #simd #performance #llvm #autovectorization
👉 @rust_lib
SIMD - это когда процессор одной инструкцией складывает не два числа, а сразу 4, 8 или 16.
Многие думают, что SIMD это сложно и требует
unsafe. Но лучший SIMD тот, который LLVM написал за вас.Допустим, у вас есть цикл:
pub fn add_arrays(a: &[f32], b: &[f32], out: &mut [f32]) {
// Простые смертные пишут так:
for i in 0..a.len() {
out[i] = a[i] + b[i];
}
}
Если вы скомпилируете это в релизе для x86_64, компилятор сгенерирует инструкции SSE2 (обрабатывая по 4 числа
f32 за раз). Почему? Потому что SSE2 гарантированно есть на всех 64-битных процессорах Intel/AMD.Как сделать еще быстрее?
Если вы знаете, что ваш код будет работать на современном железе, разрешите компилятору использовать AVX2 (по 8 чисел
f32).
RUSTFLAGS="-C target-cpu=native" cargo build --release
Но есть нюанс: Компилятор очень осторожен. Он не применит SIMD, если:
1. Есть риск паники. Если срезы разной длины, компилятор вставит проверки границ (
bounds check) внутри цикла. Это убивает векторизацию.• Лечение: Используйте итераторы (
zip) или явно проверяйте длины до цикла и используйте assert!, чтобы дать хинт оптимизатору.2. Есть риск алиасинга. Если
out пересекается в памяти с a или b.• Лечение: Rust тут молодец, его правила заимствования гарантируют отсутствие мутабельного алиасинга.
Всегда смотрите в ассемблер (
cargo-show-asm), чтобы убедиться, что цикл развернулся в SIMD-инструкции (ищите что-то вроде vaddps вместо addss).#rust #simd #performance #llvm #autovectorization
👉 @rust_lib
👍14❤8🥰1🤗1
💀 Хардкорный режим: std::arch и интринсики
Автовекторизация не сработала? Алгоритм слишком сложен для LLVM? Придется марать руки.
Встречайте
Это очень unsafe.
Это не портабельно (код для x86 не заработает на ARM M1).
Как выглядит сложение массивов на AVX2 (x86_64):
Главная опасность: Если вы вызовете эту функцию на старом процессоре (без AVX2), программа упадет с ошибкой "Illegal Instruction".
Решение: Runtime detection. Вы должны проверять фичи процессора в рантайме.
Это сложно, больно, но дает максимальный контроль над железом.
#rust #simd #unsafe #lowlevel #intrinsics
👉 @rust_lib
Автовекторизация не сработала? Алгоритм слишком сложен для LLVM? Придется марать руки.
Встречайте
std::arch. Это модуль, дающий доступ к конкретным инструкциям процессора (интринсикам).Это очень unsafe.
Это не портабельно (код для x86 не заработает на ARM M1).
Как выглядит сложение массивов на AVX2 (x86_64):
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
// Атрибут важен! Он говорит компилятору, что внутри этой функции
// можно использовать 256-битные регистры.
#[target_feature(enable = "avx2")]
unsafe fn add_avx2(a: &[f32], b: &[f32], out: &mut [f32]) {
// Мы обязаны обрабатывать данные чанками по 8 штук.
// (Опустим проверки длин и обработку "хвостов" для краткости)
for i in (0..a.len()).step_by(8) {
// Загружаем 8 float'ов в 256-битный регистр
let vec_a = _mm256_loadu_ps(a.as_ptr().add(i));
let vec_b = _mm256_loadu_ps(b.as_ptr().add(i));
// ОДНА инструкция сложения для 8 пар чисел
let vec_res = _mm256_add_ps(vec_a, vec_b);
// Выгружаем обратно в память
_mm256_storeu_ps(out.as_mut_ptr().add(i), vec_res);
}
}
Главная опасность: Если вы вызовете эту функцию на старом процессоре (без AVX2), программа упадет с ошибкой "Illegal Instruction".
Решение: Runtime detection. Вы должны проверять фичи процессора в рантайме.
fn add_smart(a: &[f32], b: &[f32], out: &mut [f32]) {
if is_x86_feature_detected!("avx2") {
// Безопасно, мы проверили
unsafe { add_avx2(a, b, out) };
} else {
// Медленный фоллбэк для старого железа
add_scalar(a, b, out);
}
}
Это сложно, больно, но дает максимальный контроль над железом.
#rust #simd #unsafe #lowlevel #intrinsics
👉 @rust_lib
👍9🔥3🙈2❤1🥰1🤗1
🚀 Portable SIMD: Пишем один раз, работает везде
В прошлом посте вы видели ужас ручных интринсиков. Представьте, что вам нужно поддерживать SSE2, AVX2, AVX-512, да еще и ARM NEON для макбуков. Это ад из
Rust сообщество решает эту проблему. Встречайте Portable SIMD (сейчас это крейт
Идея: мы используем абстрактные типы "широких" чисел, а компилятор сам мапит их на лучшие инструкции целевой платформы.
Выглядит это потрясающе чисто:
Никакого
Если вы компилируете это под AVX2 - будет AVX2. Под ARM NEON - будет NEON.
Это будущее высокопроизводительного Rust. Ждем стабилизации.
#rust #simd #nightly #future
👉 @rust_lib
В прошлом посте вы видели ужас ручных интринсиков. Представьте, что вам нужно поддерживать SSE2, AVX2, AVX-512, да еще и ARM NEON для макбуков. Это ад из
cfg_if и дублирования кода.Rust сообщество решает эту проблему. Встречайте Portable SIMD (сейчас это крейт
portable-simd или фича #![feature(portable_simd)] в nightly).Идея: мы используем абстрактные типы "широких" чисел, а компилятор сам мапит их на лучшие инструкции целевой платформы.
Выглядит это потрясающе чисто:
#![feature(portable_simd)] // Нужен nightly Rust
use std::simd::{f32x8, Simd};
fn add_portable(a: &[f32], b: &[f32], out: &mut [f32]) {
// Берем чанки по 8 элементов
let chunks_a = a.chunks_exact(8);
let chunks_b = b.chunks_exact(8);
let chunks_out = out.chunks_exact_mut(8);
for ((slice_a, slice_b), slice_out) in chunks_a.zip(chunks_b).zip(chunks_out) {
// Превращаем слайсы в SIMD-вектора (почти бесплатно)
let vec_a = f32x8::from_slice(slice_a);
let vec_b = f32x8::from_slice(slice_b);
// Обычный оператор "+" работает как SIMD сложение!
let vec_res = vec_a + vec_b;
// Записываем обратно
vec_res.copy_to_slice(slice_out);
}
// + нужно обработать "хвост", если длина не кратна 8
}
Никакого
unsafe! Никаких _mm256_ugly_names!Если вы компилируете это под AVX2 - будет AVX2. Под ARM NEON - будет NEON.
Это будущее высокопроизводительного Rust. Ждем стабилизации.
#rust #simd #nightly #future
👉 @rust_lib
🔥22❤2👍1🥰1🤗1