Skip to content

Latest commit

 

History

History
535 lines (416 loc) · 14.7 KB

File metadata and controls

535 lines (416 loc) · 14.7 KB

Поиск проблем производительности

Оглавление

  1. Неэффективные аллокации
  2. Дублирование операций
  3. Алгоритмическая сложность
  4. Избыточное копирование
  5. Блокирующие операции

Неэффективные аллокации

1. Повторные аллокации в цикле

Проблема:

// ❌ МЕДЛЕННО - миллионы аллокаций
fn process_items(items: &[Item]) -> Vec<String> {
    let mut result = Vec::new();  // Начальная capacity = 0

    for item in items {
        result.push(item.to_string());  // Каждый раз может реаллоцировать!
    }

    result
}

Как найти:

rg "Vec::new\(\)" --type rust  # Ищем Vec::new() без with_capacity

Правильно:

// ✅ БЫСТРО - одна аллокация
fn process_items(items: &[Item]) -> Vec<String> {
    let mut result = Vec::with_capacity(items.len());  // Сразу выделяем память

    for item in items {
        result.push(item.to_string());  // Никогда не реаллоцирует
    }

    result
}

// 🚀 ЕЩЁ БЫСТРЕЕ - используем итераторы
fn process_items_fast(items: &[Item]) -> Vec<String> {
    items.iter().map(|item| item.to_string()).collect()
    // collect() умный - сам выделит правильный размер
}

Измеримый эффект:

  • Для 10,000 элементов: 2-3x быстрее
  • Для 100,000 элементов: 5-10x быстрее

2. Лишние String аллокации

Проблема:

// ❌ МЕДЛЕННО - создает новые String
fn build_url(host: &str, path: &str, query: &str) -> String {
    let mut url = String::new();
    url.push_str("https://");
    url.push_str(host);
    url.push('/');
    url.push_str(path);
    url.push('?');
    url.push_str(query);
    url
}

Правильно:

// ✅ БЫСТРО - одна аллокация нужного размера
fn build_url(host: &str, path: &str, query: &str) -> String {
    let capacity = 8 + host.len() + 1 + path.len() + 1 + query.len();
    let mut url = String::with_capacity(capacity);

    url.push_str("https://");
    url.push_str(host);
    url.push('/');
    url.push_str(path);
    url.push('?');
    url.push_str(query);

    url
}

// 🚀 ИЛИ используй format! для коротких строк
fn build_url_simple(host: &str, path: &str, query: &str) -> String {
    format!("https://{}/{}?{}", host, path, query)
}

3. Создание временных Vec вместо итераторов

Проблема:

// ❌ МЕДЛЕННО - создает промежуточный Vec
fn sum_even_squares(nums: &[i32]) -> i32 {
    let evens: Vec<_> = nums.iter().filter(|&n| n % 2 == 0).collect();  // ❌ Аллокация!
    let squares: Vec<_> = evens.iter().map(|&n| n * n).collect();       // ❌ Еще одна!
    squares.iter().sum()
}

Правильно:

// ✅ БЫСТРО - zero allocations
fn sum_even_squares(nums: &[i32]) -> i32 {
    nums.iter()
        .filter(|&n| n % 2 == 0)
        .map(|&n| n * n)
        .sum()
    // Никаких промежуточных Vec - все в одном проходе!
}

Эффект:

  • Для 1,000,000 элементов: 10-20x быстрее
  • 0 аллокаций vs 2 больших аллокации

Дублирование операций

Пример из нашего проекта

Проблема:

// ❌ МЕДЛЕННО - парсим initData ДВА раза
pub fn validate_telegram_data(raw: &str, hash: &str, bot_token: &str) -> bool {
    let parsed = parse_query_string_raw(raw);  // 1-й раз

    validate_auth_date(&parsed);

    let computed_hash = compute_hash(raw, bot_token);  // Внутри еще раз парсит!
    //                                ^^^
}

fn compute_hash(raw: &str, bot_token: &str) -> String {
    let parsed = parse_query_string_raw(raw);  // 2-й раз ❌
    // ...
}

Решение:

// ✅ БЫСТРО - парсим один раз
pub fn validate_telegram_data(raw: &str, hash: &str, bot_token: &str) -> bool {
    let parsed = parse_query_string_raw(raw);  // Один раз!

    validate_auth_date(&parsed);

    let computed_hash = compute_hash(&parsed, bot_token);  // Передаем ссылку
    //                                 ^^^^^^
}

fn compute_hash(parsed: &BTreeMap<String, String>, bot_token: &str) -> String {
    // Используем уже распарсенные данные
}

Эффект:

  • -50% времени на парсинг
  • Особенно важно для горячих путей (вызывается часто)

Как найти дублирование

1. Поиск одинаковых вызовов функций:

# Найти функции, которые вызываются > 1 раза в одной функции
rg "(\w+)\(.*\).*\1\(" --type rust

2. Визуальный поиск:

  • Ищи одинаковые вычисления внутри циклов
  • Проверяй, не вызываются ли тяжелые функции несколько раз с одинаковыми аргументами

Пример:

// ❌ ПЛОХО - вычисляет len() на каждой итерации
for i in 0..items.len() {
    if i < items.len() / 2 {  // len() вызывается снова!
        // ...
    }
}

// ✅ ХОРОШО
let len = items.len();
let half = len / 2;
for i in 0..len {
    if i < half {
        // ...
    }
}

Алгоритмическая сложность

1. O(n²) вместо O(n)

Проблема:

// ❌ O(n²) - для каждого элемента проходим весь Vec
fn remove_duplicates(items: Vec<String>) -> Vec<String> {
    let mut result = Vec::new();

    for item in items {
        if !result.contains(&item) {  // ❌ O(n) для каждого элемента!
            result.push(item);
        }
    }

    result
}

Правильно:

use std::collections::HashSet;

// ✅ O(n) - используем HashSet
fn remove_duplicates(items: Vec<String>) -> Vec<String> {
    let mut seen = HashSet::new();
    let mut result = Vec::new();

    for item in items {
        if seen.insert(item.clone()) {  // O(1) в среднем
            result.push(item);
        }
    }

    result
}

// 🚀 ЕЩЁ ПРОЩЕ
fn remove_duplicates_simple(items: Vec<String>) -> Vec<String> {
    items.into_iter().collect::<HashSet<_>>().into_iter().collect()
}

Эффект:

  • Для 1,000 элементов: 100x быстрее
  • Для 10,000 элементов: 1000x быстрее

2. Линейный поиск вместо HashMap

Проблема:

// ❌ O(n) для каждого поиска
fn find_user_by_id(users: &[User], id: i64) -> Option<&User> {
    users.iter().find(|u| u.id == id)  // Проходит весь массив!
}

// В цикле - катастрофа:
for id in user_ids {
    let user = find_user_by_id(&all_users, id);  // O(n) каждый раз!
}
// Итого: O(m * n) где m = user_ids.len(), n = all_users.len()

Правильно:

use std::collections::HashMap;

// ✅ O(1) для каждого поиска
fn build_user_map(users: Vec<User>) -> HashMap<i64, User> {
    users.into_iter().map(|u| (u.id, u)).collect()
}

let user_map = build_user_map(all_users);  // O(n) один раз

for id in user_ids {
    let user = user_map.get(&id);  // O(1) каждый раз!
}
// Итого: O(n + m) вместо O(m * n)

Эффект для 10,000 поисков в массиве из 10,000 элементов:

  • Было: ~50,000,000 операций (O(m*n))
  • Стало: ~20,000 операций (O(n+m))
  • 2500x быстрее!

Как найти проблемы сложности

1. Вложенные циклы:

rg "for.*\{[\s\S]*?for" --type rust  # Ищем вложенные циклы

2. Vec::contains в цикле:

rg "\.contains\(" --type rust

3. Linear search patterns:

rg "\.iter\(\)\.find|\.iter\(\)\.position" --type rust

Вопросы к себе:

  • Сколько раз выполняется самая внутренняя операция?
  • Если данных станет в 10x больше, во сколько раз медленнее?
  • Можно ли использовать HashMap/HashSet/BTreeMap?

Избыточное копирование

1. Clone() без необходимости

Проблема:

// ❌ МЕДЛЕННО - копирует весь Vec
fn process_items(items: Vec<String>) -> usize {
    let copy = items.clone();  // ❌ Зачем?
    copy.len()
}

Как найти:

rg "\.clone\(\)" --type rust  # Проверь каждый clone!

Правильно:

// ✅ БЫСТРО - используем ссылку
fn process_items(items: &[String]) -> usize {
    items.len()
}

2. to_string() в горячем пути

Проблема:

// ❌ МЕДЛЕННО - миллионы аллокаций
fn log_items(items: &[Item]) {
    for item in items {
        tracing::debug!("Item: {}", item.id.to_string());  // ❌ Лишний to_string()
    }
}

Правильно:

// ✅ БЫСТРО - Display сам форматирует
fn log_items(items: &[Item]) {
    for item in items {
        tracing::debug!("Item: {}", item.id);  // Без to_string()
    }
}

3. Owned values вместо ссылок

Пример из нашего проекта:

// ❌ БЫЛО - копирует String'и
let mut kv_pairs: Vec<(String, String)> = parsed.into_iter()...

// ✅ СТАЛО - zero-copy
let mut kv_pairs: Vec<(&String, &String)> = parsed.iter()...

Эффект:

  • Для 100 пар ключ-значение по 20 символов: ~4KB vs ~0 байт аллокаций
  • 3-5x быстрее

Блокирующие операции

1. Блокирующий I/O в async функции

Проблема:

// ❌ БЛОКИРУЕТ весь runtime!
async fn handle_request() -> Result<String> {
    let data = std::fs::read_to_string("file.txt")?;  // ❌ Sync I/O!
    Ok(data)
}

Правильно:

// ✅ Асинхронное чтение
async fn handle_request() -> Result<String> {
    let data = tokio::fs::read_to_string("file.txt").await?;
    Ok(data)
}

// Или вынеси в blocking pool
async fn handle_request_alt() -> Result<String> {
    tokio::task::spawn_blocking(|| {
        std::fs::read_to_string("file.txt")
    }).await?
}

2. Долгие вычисления в async

Проблема:

// ❌ БЛОКИРУЕТ другие задачи
async fn expensive_computation(n: u64) -> u64 {
    (0..n).sum()  // Может работать миллисекунды/секунды
}

Правильно:

// ✅ В отдельном потоке
async fn expensive_computation(n: u64) -> u64 {
    tokio::task::spawn_blocking(move || {
        (0..n).sum()
    }).await.unwrap()
}

Инструменты профилирования

1. Cargo Flamegraph

cargo install flamegraph
cargo flamegraph --bin your-app

# Откроется flamegraph.svg - визуализация где проводится время

2. Cargo Bench

# Добавь в Cargo.toml:
# [dev-dependencies]
# criterion = "0.5"

# benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn benchmark_parse(c: &mut Criterion) {
    let data = "auth_date=123&user=test&hash=abc";

    c.bench_function("parse_query_string", |b| {
        b.iter(|| parse_query_string_raw(black_box(data)))
    });
}

criterion_group!(benches, benchmark_parse);
criterion_main!(benches);
cargo bench

3. Cargo Asm - смотри сгенерированный код

cargo install cargo-asm
cargo asm your_crate::function_name

Чеклист производительности

Аллокации

  • Vec::new() заменен на Vec::with_capacity() где известен размер?
  • Нет лишних .clone() и .to_string()?
  • Используются итераторы вместо промежуточных Vec?
  • String::with_capacity() для конкатенации?

Алгоритмы

  • Нет O(n²) где можно O(n)?
  • HashMap вместо linear search?
  • Нет вложенных циклов по большим данным?
  • Правильно выбраны структуры данных?

Копирование

  • Используются ссылки (&T) вместо owned (T)?
  • Нет лишних .clone()?
  • Избегаются .to_owned()/.to_string() в горячих путях?

Async

  • Нет блокирующих операций в async?
  • Долгие вычисления в spawn_blocking?
  • Используется async I/O?

Общее

  • Нет дублирования вычислений?
  • Профилирование показало проблемы?
  • Бенчмарки проходят в пределах нормы?

Приоритизация оптимизаций

Делай ТОЛЬКО если:

  1. Профилирование показало проблему (не оптимизируй наугад!)
  2. Это горячий путь (вызывается часто)
  3. Измеримый эффект (минимум 10% улучшение)

Не оптимизируй:

  • Холодные пути (startup, редкие операции)
  • Код, который уже достаточно быстр
  • Если это ухудшит читаемость без реального выигрыша

Правило: "Premature optimization is the root of all evil" - Donald Knuth

Но: "Premature pessimization is also evil" - правильные структуры данных с самого начала.