СОВРЕМЕННЫЕ ТЕХНОЛОГИИ РАЗРАБОТКИ ПО Лекция 9: Качество кода: Профилирование и оптимизация Мотивация • Скорость работы – часть нефункциональных требований • Недостаточная скорость – серьёзный ущерб качеству • вплоть до провала проекта • Нельзя полагаться, что завтра компьютеры станут мощнее 2 Что делать? • «Преждевременная оптимизация» • не стоит ею заниматься – пока неизвестно, где «узкое место» (bottleneck), может оказаться пустой тратой времени • но выбор алгоритмов и структур данных полезно продумывать заранее • Мониторинг эффективности • профилирование и контроль за расходом памяти • Оптимизация «узких мест» 3 Профилирование • Сбор статистики: производительность кода программы • Профилировщик – отладчик особого рода, контролирует выполнение, собирает данные и формирует отчёт: • • • • • Общее время исполнения функции/метода Удельное время исполнения Количество вызовов Структура вызовов функций (caller/callee) Места возникновения конфликтов и пенальти 4 Профилировщики • Способы сбора информации: • Event-based (вызовы, возвраты, исключения • Sampling (сбор статистики) • Instrumentation (модификация кода приложения) 5 Профилировщики: примеры • • • • • • • MSVS: штатный профилировщик Intel VTune AMD CodeAnalyst Valgrind xDebug (PHP) Python – модули cProfile, hotspot ... 6 Профилирование: тестовые данные Главные характеристики • Воспроизводимость сценария • Репрезентативность • тест максимально близок к реальному применению • Полнота охвата • может потребоваться несколько сценариев 7 Оптимизация • Всегда ищем узкое место (bottleneck) • Прибегаем к профайлерам и логам • Компромисс между оптимизацией и параллелизмом • оптимизации могут усложнять код • Контролируем результаты • повторное профилирование 8 Оптимизация: подходы В порядке приоритета: 1. Более эффективные алгоритмы и структуры данных 2. Высокоуровневые средства: • • Распараллеливание Кэширование/мемоизация, эффективная работа с памятью Низкоуровневые средства: 3. • • • • Учёт специфики платформ (размеры линии кэша, размещение инструкций, компиляторная оптимизация, векторные инструкции, и т.п.) Библиотечные функции от производителей CPU Синтаксические трюки конкретных языков Вычисления на графических картах 9 Оптимизация: Алгоритмы Затраты на выполнение растут, потому что: • Алгоритм избыточно сложен • Примеры: - fib(n) = fib(n-2) + fib(n-1) - pow(x, n) = x * pow(x, n-1) • Решение – поиск алгоритмов с меньшей вычислительной сложностью (в O-нотации) - т.е. O(n logn) в среднем лучше, чем O(n^2) - но на малых n может быть и наоборот • Задержки при доступе к памяти • последовательный доступ лучше случайного - в т.ч., оптимизируется работа с кэшем процессора • чем меньше обращений к памяти – тем лучше 10 Оптимизация: Структуры данных • Источники проблем: • Накладные расходы на работу со структурами: выделение/освобождение памяти • Сложность алгоритмов доступа к структурам - время поиска/добавления/удаления элемента, сортировки, и т.п. • На примере STL: • std::vector плох при частом добавлении/удалении элементов (особенно произвольном) - deque лучше подходит для вставки в голову/хвост - list – для вставки/удаления в произвольной позицию • последовательные контейнеры (vector, list, deque) хуже при частом поиске или поддержании порядка - лучше set/map, hash_set/hash_map • и т.п. 11 Оптимизация: Параллелизм • Эффективен на многоядерных архитектурах • Основные виды: • Параллелизм заданий - примеры: фоновые задачи (проверка орфографии, загрузка файлов из сети, и т.п.) • Параллелизм данных - один алгоритм, параллельно обрабатываются разные блоки данных 12 Оптимизация: Параллелизм • Параллелизм заданий: • Организация выполнения задач несколькими потоками • Эффективная организация выполнения: пулы потоков (thread pools) • Параллелизм данных: • Дробление данных на блоки 13 Параллелизм: ограничения • Синхронизация между потоками • Накладные расходы на запуск задач • на малых блоках может быть неэффективно • Ложное распределение памяти • два процессора пишут в одно место кэша – падает эффективность его использования • Пропускная способность памяти 14 Оптимизация: Параллелизм – OpenMP • OpenMP (Open Multi-Processing) • промышленный стандарт для компиляторов C++ • набор директив препроцессора (#pragma) для указания, как организуется параллелизм • тривиальный пример: #pragma omp parallel for for (i = 0; i < N; i++) a[i] = 2 * i; • также есть механизм запуска фоновых задач: #pragma omp parallel sections { #pragma omp section { /*some job*/ } #pragma omp section { /*some other job*/ } } 15 Оптимизация Работа с памятью • Кэширование/мемоизация: • хранение заранее подготовленных данных - для трудоёмких вычислений - для медленных/дорогих операций • загрузка из сети/с диска • использование этих значений из таблицы при повторном обращении • Примеры: • кэш картинок: «плитки» карт (gmaps/OSM), превью-картинки, и т.п. • таблица значений sin/cos в интенсивных вычислениях • промежуточные результаты при работе с документами/изображениями (для Undo) 16 Низкоуровневые оптимизации • Платформо-специфичный код: • размеры линии кэша, развёртывание циклов, размещение инструкций, предсказание ветвлений, компиляторная оптимизация, векторные инструкции, и т.п. • вплоть до ассемблерного кода • Библиотечные функции от производителей CPU • пример: функции discrete cosine transform на DSP • Синтаксические трюки конкретных языков • оптимальные способы обмена переменных, работы со строками, передачи аргументов и т.п. • Вычисления на графических картах • GPGPU: CUDA, OpenCL, DirectX 17 Ссылки и литература • Р. Гербер, А. Бик, К. Смит, К. Тиан, «Оптимизация ПО. Сборник рецептов» • С. Мейерс, «Эффективное использование STL» • раздел «Контейнеры»: http://cpp.com.ru/meyers/ch1.html • Статья «Алгоритм выбора STL-контейнера» • http://habrahabr.ru/company/infopulse/blog/194726/ • OpenMP: http://en.wikipedia.org/wiki/OpenMP • GPGPU: CUDA, OpenCL: • http://www.slideshare.net/AlexTutubalin/ss-5321298 18