Post #335
1.51K

📖 Неофициальное руководство по экосистеме Rust
https://blessed.rs/crates
https://github.com/nicoburns/blessed-rs
👉 @rust_lib
https://blessed.rs/crates
https://github.com/nicoburns/blessed-rs
👉 @rust_lib
- 🔥 8
- 👍 6
- ❤ 1
- 🥰 1
RU Showing posts older than #336 · Back to latest



entity.update()".Bevy ECS, Hecs, Legion.
struct Position { x: f32, y: f32 }
struct Velocity { dx: f32, dy: f32 }
// Пример из Bevy ECS
fn physics_system(mut query: Query<(&mut Position, &Velocity)>) {
for (mut pos, vel) in query.iter_mut() {
pos.x += vel.dx;
pos.y += vel.dy;
}
}
physics_system запрашивает данные, она бежит по памяти линейно, идеально утилизируя кэш.Vec<Particle>) забивает кэш процессора ненужными данными.
struct Particles {
positions: Vec<[f32; 3]>,
velocities: Vec<[f32; 3]>,
colors: Vec<[u8; 4]>,
lifetimes: Vec<f32>,
}
for i in 0..count {
positions[i][0] += velocities[i][0];
positions[i][1] += velocities[i][1];
positions[i][2] += velocities[i][2];
}
positions, он получает ровно позиции. Никаких имен или цветов. Ни байта не тратится впустую.
struct Particle {
position: [f32; 3], // 12 байт
velocity: [f32; 3], // 12 байт
color: [u8; 4], // 4 байта
life: f32, // 4 байта
name: String, // 24 байта
}
// Итого: 56 байт
Vec<Particle>. Это называется Array of Structs (AoS).position += velocity). Нам нужны только 24 байта из 56. Но процессор так не умеет!position, velocity, а заодно color, life и половину name. color, name), который в данном цикле не нужен. Процессору приходится постоянно бегать в медленную RAM за новыми частицами.

tokio::main, вы запускаете многопоточный рантайм (Multi-Threaded Scheduler). Под капотом создается пул потоков (воркеров), обычно по числу ядер процессора.tokio::spawn), падают в локальную очередь этого же потока.Waker и паттерн пейджераPoll::Pending). Но как рантайм узнает, что данные по сети пришли и пора снова вызвать poll()? Если он будет постоянно опрашивать все Future в цикле (while loop), ваш CPU улетит в космос на 100% загрузке.poll().epoll в Linux, kqueue в macOS или IOCP в Windows). Он ждет событий от сетевой карты или диска.Pending. Executor забывает про неё и идет выполнять другие задачи.epoll.waker.wake().poll(), и стейт-машина делает следующий шаг.enumasync запускает легковесный поток (горутину). Забудьте. Асинхронный Rust работает совершенно иначе - он не аллоцирует стек под каждую задачу.async fn, компилятор превращает вашу функцию в конечный автомат (State Machine).
async fn fetch_data() {
let req = build_request();
let resp = send(req).await; // <-- Точка остановки 1
process(resp).await; // <-- Точка остановки 2
}
enum, который хранит локальные переменные между вызовами .await:
// Примерно так это видит компилятор:
enum FetchDataStateMachine {
Start,
WaitSend { req: Request }, // Храним локальные переменные
WaitProcess { resp: Response },
Done,
}
poll() у этой Future:.await.WaitSend).enum весит считанные байты. Вы можете создать миллион таких Future на обычном ноутбуке, и они займут всего пару десятков мегабайт оперативки. Никакая ОС не позволит вам создать миллион настоящих потоков.cfg_if и дублирования кода.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!std::arch. Это модуль, дающий доступ к конкретным инструкциям процессора (интринсикам).
#[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);
}
}
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);
}
}
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];
}
}
f32 за раз). Почему? Потому что SSE2 гарантированно есть на всех 64-битных процессорах Intel/AMD.f32).
RUSTFLAGS="-C target-cpu=native" cargo build --release
bounds check) внутри цикла. Это убивает векторизацию.zip) или явно проверяйте длины до цикла и используйте assert!, чтобы дать хинт оптимизатору.out пересекается в памяти с a или b.cargo-show-asm), чтобы убедиться, что цикл развернулся в SIMD-инструкции (ищите что-то вроде vaddps вместо addss).if: Branchless Programmingif - это отсутствие if.a, если condition true, и b, если false.
let result = if condition { a } else { b };
// Генерирует инструкцию перехода (JMP/JNE) -> Риск misprediction
bool можно скастить в u8 (true = 1, false = 0).
// (a * 1) + (b * 0) = a
// (a * 0) + (b * 1) = b
let result = a * (condition as i32) + b * (!condition as i32);
if в инструкцию CMOV (Conditional Move). Это инструкция процессора: "Скопируй данные, если флаг выставлен". Это не ветвление, это просто копирование по условию.godbolt или cargo-show-asm). Если видите cmov - поздравляю, у вас branchless код, и он будет работать быстро, даже если данные случайны. Если видите jle / jne - это прыжок.#[cold] и likely#[cold]
#[cold] // <-- Подсказка
fn handle_fatal_error() {
// Логирование, паника, сложная логика...
}
fn process(data: &str) {
if let Err(_) = parse(data) {
handle_fatal_error(); // Компилятор знает, что это маловероятно
return;
}
// Happy path идет дальше линейно
}
likely / unlikely (Nightly / Crates)std::intrinsics::likely, но есть крейт likely или хаки. Это прямая инструкция процессору через LLVM.
// С крейтом `likely`
if likely(x > 0) {
// Оптимизатор будет строить код так,
// будто это условие почти всегда true
}
#[cold] для ошибок и граничных случаев. Это стабильно, бесплатно и делает код чище.if (ветвление).then или в else, пока не вычислит условие. Но ждать он не может - конвейер встанет. Поэтому он угадывает.
// Если data отсортирован: T T T T T F F F F F (паттерн ясен)
// Если data случайный: T F T T F F T F (паттерн непредсказуем)
for &x in &data {
if x > 128 {
sum += x;
}
}
data.sort() перед обработкой окупается с лихвой.