Что делать, если код работает медленно?cplus-6

Когда код работает медленно, важно применять методичный подход к анализу и оптимизации. Вот профессиональная стратегия, которую я использую на практике.

1. Профилирование перед оптимизацией

Золотое правило: Не оптимизировать без профилирования.

Инструменты:

  • Linux: perf, gprof, Valgrind/Callgrind
  • Windows: VTune, Windows Performance Toolkit
  • Кроссплатформенные: Google Benchmark, Tracy

Пример использования perf:

perf record ./my_program
perf report

2. Анализ горячих точек

Что искать:

  • Функции с наибольшим временем выполнения
  • Часто вызываемые функции
  • Неожиданные узкие места

Пример вывода:

Overhead  Command  Shared Object       Symbol
  35.12%  my_app   my_app              [.] _Z12process_dataPKdi
  22.45%  my_app   libc.so.6           [.] malloc
  18.76%  my_app   my_app              [.] _Z9heavy_mathRi

3. Оптимизация алгоритмов

Первый шаг: Убедиться в оптимальности алгоритмов.

Проблемные паттерны:

  • Неоптимальные структуры данных
  • Квадратичные алгоритмы там, где возможны O(n log n)
  • Избыточные вычисления

Пример оптимизации:

// Было: O(n^2)
for (auto& item : items) {
    if (std::find(selected.begin(), selected.end(), item.id) != selected.end()) {
        process(item);
    }
}

// Стало: O(n)
std::unordered_set<int> selected_set(selected.begin(), selected.end());
for (auto& item : items) {
    if (selected_set.count(item.id)) {
        process(item);
    }
}

4. Оптимизация работы с памятью

Ключевые проблемы:

  • Избыточные аллокации
  • Кэш-промахи
  • Неэффективная локализация данных

Пример улучшения:

// Было: множественные аллокации
std::vector<std::string> process() {
    std::vector<std::string> result;
    for (/*...*/) {
        std::string item = create_item();
        result.push_back(item); // возможны реаллокации
    }
    return result;
}

// Стало: резервирование памяти
std::vector<std::string> process() {
    std::vector<std::string> result;
    result.reserve(expected_count); // предотвращаем реаллокации
    for (/*...*/) {
        result.emplace_back(create_item()); // конструируем на месте
    }
    return result; // NRVO или move
}

5. Векторизация и параллелизация

Современные подходы:

  • Автовекторизация компилятором
  • Явное использование SIMD (SSE, AVX)
  • Многопоточность

Пример SIMD:

#include <immintrin.h>

void sum_arrays(float* a, float* b, float* c, size_t n) {
    for (size_t i = 0; i < n; i += 8) {
        __m256 av = _mm256_load_ps(a + i);
        __m256 bv = _mm256_load_ps(b + i);
        __m256 cv = _mm256_add_ps(av, bv);
        _mm256_store_ps(c + i, cv);
    }
}

6. Оптимизация ввода-вывода

Проблемные места:

  • Частые мелкие операции ввода-вывода
  • Синхронные операции там, где можно асинхронные
  • Избыточные буферизации

Пример улучшения:

// Было: посимвольное чтение
std::ifstream file("data.bin");
char c;
while (file.get(c)) { /*...*/ }

// Стало: буферизованное чтение
std::vector<char> buffer(1024 * 1024);
while (file.read(buffer.data(), buffer.size())) {
    size_t read = file.gcount();
    // Обработка блока данных
}

7. Микрооптимизации

Эффективные приёмы:

  • Инлайнинг критических функций
  • Оптимизация условных переходов
  • Развертка циклов
  • Битовые операции вместо арифметических

Пример:

// Было: деление в цикле
for (int i = 0; i < n; ++i) {
    array[i] = i / divisor;
}

// Стало: умножение на обратное
float inv_divisor = 1.0f / divisor;
for (int i = 0; i < n; ++i) {
    array[i] = i * inv_divisor;
}

8. Оптимизация компилятора

Флаги для gcc/clang:

  • -O3 - агрессивная оптимизация
  • -march=native - использование специфичных инструкций CPU
  • -flto - межмодульная оптимизация

Важно: Всегда проверять, что оптимизации не ломают логику!

9. Кэш-дружественный код

Принципы:

  • Локализация данных
  • Предсказуемый доступ к памяти
  • Оптимальный размер структур

Пример:

// Плохо для кэша:
struct Item {
    int id;
    double data[100];
    bool active;
};

// Лучше:
struct Item {
    int id;
    bool active;
    double data[100]; // Большие поля в конец
};

10. Проверка ассемблерного вывода

Как анализировать:

g++ -O3 -S -masm=intel main.cpp -o main.s

Что искать:

  • Лишние операции загрузки/сохранения
  • Неожиданные вызовы функций
  • Отсутствие векторизации

Резюмируем

Когда код работает медленно:

  1. Измеряйте - найдите реальные узкие места
  2. Анализируйте - алгоритмы, память, ввод-вывод
  3. Оптимизируйте осознанно - от высокоуровневых изменений к низкоуровневым
  4. Проверяйте - что оптимизации дали эффект
  5. Документируйте - причины выбранных оптимизаций

Помните: "Преждевременная оптимизация - корень всех зол" (Д. Кнут), но и запоздалая оптимизация может быть дорогостоящей. Балансируйте между читаемостью и производительностью.