Измерение производительности кода и создание базовых показателей
Измерение производительности кода и создание базовых показателей является критическим шагом в процессе оптимизации. Он предоставляет эталонную точку для оценки результатов внесенных изменений.
Создание базовых показателей производительности
Базовый показатель производительности — это набор измерений, который фиксирует выполнение кода перед внесением изменений. Это ваша точка ссылки. Без базового плана для сравнения, вы не знаете, улучшают ли обновления производительность кода или делают вещи хуже.
Ниже приведены некоторые ключевые аспекты измерения в базовом плане:
Время выполнения (задержка): Сколько времени занимает выполнение определенной операции или процесса? Это значение можно измерить для небольшой функции (например, для сортировки 10 000 элементов) или сквозного сценария (например, служба отвечает на запрос за 250 миллисекунд).
Производительность: Сколько операций можно выполнить за определенное время? Это значение относится к серверным приложениям (запросам в секунду), пакетной обработке (записи обрабатываются в минуту) и т. д.
Использование ресурсов: Потребление памяти (рабочий набор, распределение, частота сборки мусора), использование ЦП (привязывает ли его ядро к 100%? как долго?), дисковые операции ввода-вывода, сетевые операции ввода-вывода и т. д., в зависимости от того, что делает код.
Характеристики масштабируемости: Как изменяются указанные выше метрики по мере увеличения размера входных данных или по мере увеличения нагрузки (одновременных пользователей) ? Можно измерить, что функция принимает 0,1 миллисекунда для 100 элементов и 10 миллисекунд для 10 000 элементов. Это указывает, как она масштабируется (в данном случае примерно линейная с количеством элементов). Такой базовый план помогает понять масштабируемость и задать цели. Например, может потребоваться отсортировать 100 тысяч элементов в 200 миллисекундах, что может означать, что вам потребуется более эффективный алгоритм или параллельная сортировка.
При создании базового плана:
Сделайте среду максимально согласованной. Выполняйте тесты на одном компьютере в аналогичных условиях (закрывайте фоновые программы, используйте сборку выпуска и т. д.). Производительность может варьироваться от запуска к запуску; минимизация шума позволяет получить более надежные данные.
Используйте релизные сборки и реалистичные параметры. Отладка сборок часто выполняется медленнее из-за дополнительных проверок и отсутствия оптимизации. Если вы измеряете скорость алгоритма, скомпилируйте в режиме Release с оптимизациями. Аналогичным образом, если ваш код является многопоточным или асинхронным, протестируйте его в условиях, приближенных к рабочей среде (например, с тем же количеством потоков или с тем же размером набора данных).
Автоматизация или скрипт измерения. Код следует запускать несколько раз. Удобно иметь простой инструментарий для оценки производительности или использовать средства, которые автоматизируют этот процесс. Например, для небольшой функции можно написать цикл, который вызывает его, время измерения и вычислить среднее значение.
Запишите числа. Сохраняйте запись базовых метрик (время, память и т. д.). Если у вас есть модульный тест или выходные данные журнала, которые печатают сведения о производительности, сохраните это. Для нетривиальных случаев использование электронной таблицы или простой таблицы для сравнения "до и после" полезно.
Базовое измерение с помощью секундомера
C# предлагает System.Diagnostics.Stopwatch класс, который полезен для измерения времени выполнения блоков кода.
Например, предположим, что у вас есть следующий методProcessData(), который вы подозреваете, что он работает медленно:
var sw = Stopwatch.StartNew();
ProcessData();
sw.Stop();
Console.WriteLine($"ProcessData() completed in {sw.ElapsedMilliseconds} milliseconds");
Представьте, что этот код последовательно выводит значение примерно 120 миллисекунд. Среднее значение является вашей базовой линией для ProcessData() при условиях теста, которые вы использовали.
Базовые показатели использования памяти
Метод GC.GetTotalMemory — это вызов метода .NET, который извлекает текущий объем памяти, выделенный сборщиком мусора (GC). Этот GetTotalMemory метод фиксирует текущее состояние выделения памяти при вызове.
Вы можете записать использование памяти, проверив GC.GetTotalMemory(false) до и после операции.
Например, предположим, что у вас есть следующий код:
long memoryBefore = GC.GetTotalMemory(forceFullCollection: true);
ProcessData();
long memoryAfter = GC.GetTotalMemory(forceFullCollection: true);
Console.WriteLine($"Memory used by ProcessData(): {memoryAfter - memoryBefore} bytes");
Если этот код печатает "Память, используемая ProcessData(): 50 000 байт", это объем памяти, выделенной операцией (net). Это значение может быть частью базового плана, особенно если вы беспокоитесь о затратах на память или сборку мусора.
Контекстные базовые показатели
Базовое значение часто не является одним числом. Возможно, у вас есть базовые показатели в определенном масштабе, а затем они могут быть протестированы на более крупном уровне. Рассмотрим пример.
- Сортировка 10 тыс. элементов занимает 50 миллисекунд (по умолчанию для 10 тыс.).
- Сортировка 100k элементов занимает 800 миллисекунда.
Это означает, что производительность снижается по мере роста входных данных, что ожидается. Он также предполагает, что алгоритм может быть хуже O(n log n), так как увеличение элементов на коэффициент 10, как представляется, увеличивает время на 16. Этот тип базовых показателей помогает понять масштабируемость и задать цели.
Средства и методы измерения производительности
Для понимания производительности требуется как измерение, так и анализ алгоритмических характеристик. Перед погружением в средства измерения важно понимать основные понятия производительности:
Основные сведения о алгоритмической сложности
Временная сложность описывает, как время выполнения алгоритма растет по мере увеличения размера входных данных, как правило, выражается с помощью нотации Большого O.
- O(1) — константное время: производительность не изменяется от размера входных данных (например, поиск значений в словаре).
- O(n) — линейное время: производительность увеличивается пропорционально с увеличением объёма входных данных (например, при однократном проходе по списку).
- O(n²) — квадратное время: производительность растет с квадратом входного размера (например, вложенные циклы).
- O(log n) — логарифмическое время: производительность растет медленно по мере увеличения входных данных (например, двоичного поиска).
Сложность пространства измеряет, как увеличивается использование памяти с размером входных данных, используя ту же нотацию Big O. Понимание этих понятий помогает определить, почему определенные шаблоны кода становятся узкими местами в масштабе.
Распознавание шаблонов производительности и анти-шаблонов
К общим антипаттернам производительности относятся:
- Проблемы с запросами N+1: создание одного запроса для получения списка, а затем N больше запросов для связанных данных.
- Неэффективные структуры данных: использование списков для частых подстановок вместо словарей или хэш-наборов.
-
Преждевременное объединение строк: создание больших строк с использованием
+=вместоStringBuilder. - Синхронные операции: блокировка потоков с синхронными операциями ввода-вывода, когда асинхронные операции будут лучше.
Стратегии кэширования могут значительно повысить производительность, сохраняя часто доступ к данным в памяти, избегая дорогостоящих операций повторной компиляции или операций ввода-вывода.
С простых до сложных, ниже перечислены средства и методы для измерения производительности:
Ручное время с помощью Stopwatch
Код, вручную инструментированный с помощью Stopwatch или даже с различиями в DateTime.UtcNow, может дать быструю информацию. Этот подход является импровизированным и доступным, но часто достаточным для первого исследования. Журналы времени можно сыпать по разделам кода (например, сколько времени занимает запрос базы данных, сколько времени потребовалось синтаксический анализ файлов и т. д.).
Ведение журнала и счетчики
Журналирование осуществляется на неформальной основе, но доступно и часто является достаточным для первого исследования. Журналы времени можно сыпать по разделам кода (например, сколько времени занимает запрос базы данных, сколько времени потребовалось синтаксический анализ файлов и т. д.).
Ведение журнала событий и метрики производительности
Добавление стратегического журналирования может выявлять шаблоны производительности и определять узкие места.
- Счетчики операций: журнал количества обработанных элементов, выполненных запросов к базе данных, попаданий в кэш и промахов.
- Разбивки по времени: измеряйте различные этапы операций (например, "Запрос базы данных: 50 миллисекундах, обработка: 20 миллисекундах, сериализация: 10 миллисекундах").
- Использование ресурсов: отслеживайте выделение памяти, использование пула потоков или метрики пула подключений.
- Метрики производительности: мониторинг задержки (время отклика), пропускная способность (операции в секунду) и характеристики масштабируемости.
Например, если в журнале отображается сообщение "Получение 1000 записей из базы данных", когда ожидается 100 записей, несоответствие может указывать на проблему запроса N+1 или неэффективную логику запроса. Аналогичным образом, запись частоты попадания в кэш может помочь оценить эффективность стратегии кэширования.
Встроенные профилировщики (диагностика Visual Studio)
Visual Studio (выпуск Enterprise и в некоторой степени Community) имеет средства профилирования (использование ЦП, использование памяти, профилировщик производительности). Приложение можно запустить под профилировщиком, и оно показывает, например, разбивку времени ЦП по функциям или список объектов в куче и кто их выделил.
- Профилировщик ЦП обычно создает дерево вызовов, где можно увидеть, какие методы потребляли больше всего времени ЦП.
- Профилировщик памяти может создавать моментальные снимки, чтобы показать, как увеличивается потребление памяти, и какие типы объектов занимают место.
Использование профилировщика часто так же просто, как нажатие кнопки "Пуск диагностики" и выбор типа профиля.
Средства .NET CLI
Для .NET Core и .NET 5+ корпорация Майкрософт предоставляет такие средства командной строки, как dotnet-counters, dotnet-trace, dotnet-dumpdotnet-gcdump.
-
dotnet-countersможет отображать метрики производительности запущенного приложения в режиме реального времени (коллекции GC, исключения, использование пула потоков и т. д.). -
dotnet-traceможет собирать трассировку выполнения приложения, которую можно проанализировать, чтобы узнать, какие методы выполнялись.
Эти инструменты являются более сложными, но незаменимыми для глубокого погружения или профилирования в производственной среде, где вы не можете использовать профилировщик графического интерфейса.
Библиотеки для микробенчмаркинга
Если вы хотите сравнить две реализации функции (например, исходную и предлагаемую оптимизированную версию) строго, BenchmarkDotNet — это популярная библиотека. Он выполняет функции много раз, разогревает JIT-компилятор, измеряет точно, и дает статистику (например, среднее, стандартное отклонение). Эти данные используются для микро-тестов (небольших изолированных путей кода) с высокой точностью.
Производительность и нагрузочные тесты
В более крупных сценариях (веб-приложения, службы) можно написать нагрузочный тест или использовать средство (например, JMeter, k6 или нагрузочный тест Visual Studio) для имитации множества запросов или больших входных данных. Этот подход может выявить пропускную способность и стабильность при нагрузке, выявляя узкие места, которые проявляются только при масштабировании.
Мониторинг системы
Отслеживайте общее поведение системы, чтобы определить узкие места ресурсов и проблемы с масштабируемостью:
- Шаблоны использования ЦП: высокая загрузка ЦП может указывать на алгоритмические неэффективные или вычислительные узкие места.
- Потребление памяти. Увеличение использования памяти может предложить утечки памяти, неэффективные структуры данных или чрезмерное выделение объектов.
- Метрики ввода-вывода: высокий объем дисков или сетевых операций ввода-вывода может указывать на неэффективные шаблоны доступа к данным или плохое кэширование.
- Сборка мусора: частые сборки мусора могут повлиять на производительность, особенно в приложениях с высокой пропускной способностью.
Такие инструменты, как диспетчер задач или PerfMon в Windows, dotnet-countersмогут предоставлять аналитические сведения на уровне системы. Понимание этих метрик помогает сопоставить производительность на уровне кода с использованием системных ресурсов.
Мониторинг улучшений и предотвращение регрессии
После создания базового плана можно начать реализацию улучшений кода. Улучшения кода должны быть сделаны в цикле обновлений кода, за которым следует измерение, чтобы узнать, было ли изменение необходимым эффектом.
При внесении изменений всегда измеряйте тем же методом, который использовался в базовых измерениях. Сравните новые измерения с базовыми показателями.
Ниже приведены некоторые рекомендации по этому процессу.
Измените одну вещь за раз (если это возможно): Поэтому вы знаете, что вызвало любое улучшение или регрессию.
Выполните тот же тест или измерение, что и базовый план: Используйте ту же процедуру и напрямую сравнивайте метрики.
Если достигнуто улучшение: Отлично, подумайте, соответствует ли оно цели. Если вам потребуется ускорение в 2 раза, а вы получили только 1,5 раза, вы можете продолжить итерации.
Если нет улучшений или наблюдается ухудшение производительности: разберитесь, почему. Может быть, ваше изменение не решало реальной проблемы узкого места или заменяло одну стоимость на другую.
Проверьте наличие побочных эффектов: Побочные эффекты могут включать непредвиденные проблемы с корректностью или производительностью. Например, можно оптимизировать производительность ЦП, но наблюдать за большим увеличением использования памяти. Является ли этот побочный эффект приемлемым?
Автоматизация обнаружения регрессии: Написание модульных тестов производительности или тестов, выполняемых до и после изменений, является общим методом для обнаружения регрессии. Хотя не каждая команда записывает тесты производительности, это не плохая идея иметь небольшой набор тестов производительности (особенно для критически важных путей), которые гарантируют, что новые изменения не резко замедляют работу.
Использование GitHub Copilot для помощи в измерениях
GitHub Copilot поможет вам настроить методы измерения производительности:
- Попросите примеры измерения времени: "Как я могу измерить время выполнения метода C#?" GitHub Copilot может предложить
Stopwatchс примерами кода. - Получите руководство по профилированию: "Какие средства можно использовать для профилирования приложения .NET?" GitHub Copilot может перечислять средства профилирования и их варианты использования.
- Создание кода теста: GitHub Copilot может помочь создать классы BenchmarkDotNet или другие средства измерения.
GitHub Copilot служит кратким справочником для правильной реализации техник измерения.
Сводка
Измерение производительности кода и создание базовых показателей является критическим шагом в процессе оптимизации. Систематически фиксируя время выполнения, использование ресурсов и характеристики масштабируемости, вы создаете эталонную точку для оценки влияния изменений. Использование различных методов измерения, от простого измерения времени с помощью Stopwatch, до использования расширенных инструментов профилирования, позволяет точно определить узкие места. Всегда убедитесь, что улучшение производительности не подвергает опасности правильность и не вводит новые проблемы. Благодаря дисциплинированному подходу к измерению и итерации можно эффективно повысить производительность кода при сохранении его целостности.