🐮 Cow: Ленивое клонирование
Все знают: лишний
Допустим, мы чистим инпут от спецсимволов.
1. Если спецсимволов нет - зачем аллоцировать новую строку? Можно вернуть ссылку на исходную.
2. Если есть - придется создать новую
Здесь на сцену выходит
Это мощнейший инструмент для хот-пасов (hot paths), где 90% данных не требуют изменений. Не ленитесь использовать
#rust #performance #std
👉 @rust_lib
Все знают: лишний
clone() - это грех. Но иногда нам нужно вернуть строку, которая может быть изменена, а может и нет.Допустим, мы чистим инпут от спецсимволов.
1. Если спецсимволов нет - зачем аллоцировать новую строку? Можно вернуть ссылку на исходную.
2. Если есть - придется создать новую
String.Здесь на сцену выходит
std::borrow::Cow (Clone on Write).
use std::borrow::Cow;
fn sanitize(input: &str) -> Cow<str> {
if input.contains('!') {
// Аллокация происходит только тут
Cow::Owned(input.replace('!', "?"))
} else {
// Тут мы просто возвращаем ссылку. Zero allocation.
Cow::Borrowed(input)
}
}
Это мощнейший инструмент для хот-пасов (hot paths), где 90% данных не требуют изменений. Не ленитесь использовать
Cow там, где можно избежать лишних аллокаций.#rust #performance #std
👉 @rust_lib
👍32❤5🥰1😁1🤗1
Почему
📈 Векторный рост: Избегаем реаллокаций
Мы часто пишем:
Что происходит под капотом?
1. Вектор создается пустым (0 байт).
2. Первый
3. Пятый
• Создается новый буфер (размером x2).
• Все старые элементы копируются туда.
• Старый буфер освобождается.
• Новый элемент записывается.
Это называется Reallocation. Это дорого. Если у вас 10 миллионов элементов, вы сделаете десятки реаллокаций и скопируете гигабайты данных просто так.
Решение: Если вы хотя бы примерно знаете размер - подскажите компилятору.
Правило:
#rust #performance #vectors
👉 @rust_lib
Vec::new() это ловушка для производительности.📈 Векторный рост: Избегаем реаллокаций
Мы часто пишем:
let mut vec = Vec::new();
for item in heavy_iter {
vec.push(item);
}
Что происходит под капотом?
1. Вектор создается пустым (0 байт).
2. Первый
push: аллокация на 4 элемента (условно).3. Пятый
push: места нет.• Создается новый буфер (размером x2).
• Все старые элементы копируются туда.
• Старый буфер освобождается.
• Новый элемент записывается.
Это называется Reallocation. Это дорого. Если у вас 10 миллионов элементов, вы сделаете десятки реаллокаций и скопируете гигабайты данных просто так.
Решение: Если вы хотя бы примерно знаете размер - подскажите компилятору.
// Идеально, если знаем точно
let mut vec = Vec::with_capacity(exact_count);
// Или используем итераторы, они часто сами знают свой размер
let vec: Vec<_> = heavy_iter.collect();
collect() умный. Он смотрит на size_hint итератора и сразу аллоцирует нужный буфер (если итератор "честный").Правило:
Vec::new() - только если вы правда не знаете, будет ли там хоть один элемент. В остальных случаях - with_capacity.#rust #performance #vectors
👉 @rust_lib
👍31❤6🥰2😢2🤗1
🗝 Турбируем HashMap
Стандартный
Почему? Он криптографически стоек к HashDoS атакам (когда злоумышленник подбирает ключи так, чтобы все они попадали в один бакет, превращая мапу в связный список и убивая CPU).
Но SipHash медленный. Если вы решаете алгоритмические задачи, пишете геймдев или у вас внутренний сервис без внешнего доступа - вам не нужна защита от DoS. Вам нужна скорость.
Используйте сторонние хэшеры. Самые популярные:
1.
2.
Пример с крейтом
Прирост производительности на операциях вставки/поиска может достигать 2x-3x на простых ключах.
Итог:
• Backend наружу? Оставляем дефолтный
• GameDev / Algo / Internal Data Processing? Ставим
#rust #performance #hashmap #external_crates
👉 @rust_lib
Стандартный
std::collections::HashMap в Rust использует алгоритм хэширования SipHash.Почему? Он криптографически стоек к HashDoS атакам (когда злоумышленник подбирает ключи так, чтобы все они попадали в один бакет, превращая мапу в связный список и убивая CPU).
Но SipHash медленный. Если вы решаете алгоритмические задачи, пишете геймдев или у вас внутренний сервис без внешнего доступа - вам не нужна защита от DoS. Вам нужна скорость.
Используйте сторонние хэшеры. Самые популярные:
1.
FxHash (rustc-hash) - используется внутри самого компилятора Rust и Firefox. Молниеносно быстрый для коротких ключей (integers).2.
AHash - современный, быстрый, использует аппаратные инструкции AES.Пример с крейтом
rustc-hash:
use rustc_hash::FxHashMap;
// Это та же HashMap, но с быстрым хэшером
let mut map: FxHashMap<u32, &str> = FxHashMap::default();
Прирост производительности на операциях вставки/поиска может достигать 2x-3x на простых ключах.
Итог:
• Backend наружу? Оставляем дефолтный
SipHash.• GameDev / Algo / Internal Data Processing? Ставим
FxHash или AHash.#rust #performance #hashmap #external_crates
👉 @rust_lib
👍17❤6🥰2✍1🏆1
🔮 Ты не пройдешь: Магия предсказателя ветвлений
Вы когда-нибудь задумывались, почему обработка отсортированного массива чисел происходит в разы быстрее, чем случайного, даже если логика одна и та же?
Всё дело в конвейере (pipeline). Процессор выполняет инструкции не по одной, а потоком: пока одна декодируется, другая уже выполняется. Но тут появляется
Процессор не знает, пойдет код в
• Угадал? Выполнение продолжается без задержек.
• Не угадал? Происходит Pipeline Flush. Процессор выбрасывает все инструкции, которые успел "набрать" по неверному пути, и начинает заново с правильного адреса. Это огромная потеря тактов (10-20 циклов CPU).
Пример на Rust:
На случайных данных BPU ошибается в 50% случаев. На сортированных почти никогда.
Вывод: Чем предсказуемее ваши данные, тем быстрее работает ваш код. Иногда
#rust #cpu #lowlevel #performance #branch_prediction
👉 @rust_lib
Вы когда-нибудь задумывались, почему обработка отсортированного массива чисел происходит в разы быстрее, чем случайного, даже если логика одна и та же?
Всё дело в конвейере (pipeline). Процессор выполняет инструкции не по одной, а потоком: пока одна декодируется, другая уже выполняется. Но тут появляется
if (ветвление).Процессор не знает, пойдет код в
then или в else, пока не вычислит условие. Но ждать он не может - конвейер встанет. Поэтому он угадывает.• Угадал? Выполнение продолжается без задержек.
• Не угадал? Происходит Pipeline Flush. Процессор выбрасывает все инструкции, которые успел "набрать" по неверному пути, и начинает заново с правильного адреса. Это огромная потеря тактов (10-20 циклов CPU).
Пример на Rust:
// Если 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;
}
}
На случайных данных BPU ошибается в 50% случаев. На сортированных почти никогда.
Вывод: Чем предсказуемее ваши данные, тем быстрее работает ваш код. Иногда
data.sort() перед обработкой окупается с лихвой.#rust #cpu #lowlevel #performance #branch_prediction
👉 @rust_lib
👍21👎3✍2❤1🥰1
🚫 Убиваем
Самый быстрый
Иногда можно заменить ветвление на арифметику. Это гарантирует отсутствие Pipeline Flush, так как поток инструкций линейный.
Задача: Вернуть
С ветвлением:
Branchless (без ветвления):
В Rust
Но подождите!
Современные компиляторы часто делают это сами. Они превращают простой
Как проверить?
Смотрите в ассемблер (через
Совет: Пишите читаемый код. И только если профайлер показывает, что BPU захлебывается в горячем цикле - переписывайте на арифметику.
#rust #assembly #branchless #performance
👉 @rust_lib
if: Branchless ProgrammingСамый быстрый
if - это отсутствие if.Иногда можно заменить ветвление на арифметику. Это гарантирует отсутствие Pipeline Flush, так как поток инструкций линейный.
Задача: Вернуть
a, если condition true, и b, если false.С ветвлением:
let result = if condition { a } else { b };
// Генерирует инструкцию перехода (JMP/JNE) -> Риск misprediction
Branchless (без ветвления):
В Rust
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 - это прыжок.Совет: Пишите читаемый код. И только если профайлер показывает, что BPU захлебывается в горячем цикле - переписывайте на арифметику.
#rust #assembly #branchless #performance
👉 @rust_lib
👍14❤3🔥2✍1🥰1🤗1
🏎️ 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
📦 Иллюзия объектов: Как мы морим процессор голодом
Представьте, что вы пишете симуляцию (или игру). По заветам ООП вы создаете сущность:
Затем складываете их в массив:
Теперь нам нужно в цикле обновить позицию (
Процессор читает память из RAM в свой сверхбыстрый L1 кэш кусками по 64 байта (Cache Line).
Когда он берет первую частицу, он затягивает в кэш её
Результат:
1. Cache Miss: Больше половины кэша забито мусором (
2. Bandwidth: Вы сжигаете пропускную способность памяти впустую.
Мы пишем код для людей, забывая, что исполнять его железу. Железо любит однородные, плотно упакованные данные. Как это исправить? Об этом в следующем посте.
#rust #architecture #dop #performance #cpu
👉 @rust_lib
Представьте, что вы пишете симуляцию (или игру). По заветам ООП вы создаете сущность:
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. Но процессор так не умеет!Процессор читает память из RAM в свой сверхбыстрый L1 кэш кусками по 64 байта (Cache Line).
Когда он берет первую частицу, он затягивает в кэш её
position, velocity, а заодно color, life и половину name. Результат:
1. Cache Miss: Больше половины кэша забито мусором (
color, name), который в данном цикле не нужен. Процессору приходится постоянно бегать в медленную RAM за новыми частицами.2. Bandwidth: Вы сжигаете пропускную способность памяти впустую.
Мы пишем код для людей, забывая, что исполнять его железу. Железо любит однородные, плотно упакованные данные. Как это исправить? Об этом в следующем посте.
#rust #architecture #dop #performance #cpu
👉 @rust_lib
👍22❤3👎1🔥1💯1