Idiomati c способ оценки производительности? - PullRequest
0 голосов
/ 19 февраля 2020

Я оцениваю сеть + рабочую нагрузку рендеринга для моего проекта.

Программа постоянно запускает основной l oop:

while (true) {
   doSomething()
   drawSomething()
   doSomething2()
   sendSomething()
}

Основной l oop работает более чем 60 раз в секунду.

Я хочу увидеть разбивку производительности, сколько времени занимает каждая процедура.

Меня беспокоит то, что если я распечатаю интервал времени для каждого входа и выхода каждой процедуры ,

Это может привести к огромным потерям производительности.

Мне любопытно, что такое идиоматический c способ измерения производительности.

Достаточно ли печатать протоколирование?

1 Ответ

2 голосов
/ 19 февраля 2020

Обычно: для повторяющихся коротких вещей вы можете просто рассчитать время всего повтора l oop. (Но микробенчмаркинг сложен; легко исказить результаты, если вы не понимаете последствий этого.)

Или, если вы настаиваете на синхронизации каждой отдельной итерации, запишите результаты в массив и напечатайте позже; Вы не хотите вызывать печатный код с большим весом внутри своего l oop.

. Этот вопрос слишком широк, чтобы сказать что-либо более конкретное c.

Многие языки имеют пакеты для тестирования, которые помогут вам написать микробенчмарки для одной функции. Используйте их . например, для Java JMH проверяет, что тестируемая функция разогревается и полностью оптимизируется JIT и всем этим джазом, прежде чем выполнять синхронизированные запуски. И запускает его в течение указанного интервала, подсчитывая, сколько итераций он завершает.

Остерегайтесь распространенных ошибок микробенчмарка:

  • Ошибка прогрева кэшей кода / данных и прочее: ошибки страницы в пределах временная область для прикосновения к новой памяти или кэш-памяти кода / данных, которая не будет частью нормальной работы. (Пример того, как заметить этот эффект: Производительность: memset ; пример неправильного вывода на основании этой ошибки )
  • Невозможность дать процессору время нарастания вплоть до макс. турбо: современные процессоры сбрасывают тактовую частоту вращения на холостые обороты, чтобы сэкономить электроэнергию, и работают только через несколько миллисекунд. (Или дольше в зависимости от ОС / HW).

    связано: на современном x86, RDTS C подсчитывает опорные циклы, а не тактовые частоты ядра , поэтому он подвержен тому же Эффекты изменения частоты, такие как время настенных часов.

  • На современных процессорах с не по порядку выполнением некоторые вещи слишком коротки, чтобы по-настоящему иметь смысл , см. также это . Производительность крошечного блока ассемблера (например, сгенерированного компилятором для одной функции) не может быть охарактеризована одним числом, , даже если он не разветвляется или не обращается к памяти (поэтому нет вероятности ошибочного прогнозирования). или отсутствует кеш). Он имеет задержку от входов к выходам, но разная пропускная способность при многократном запуске с независимыми входами выше. например, инструкция add на процессоре Skylake имеет пропускную способность 4 / такт, но задержка 1 цикл. Так что dummy = foo(x) может быть в 4 раза быстрее, чем x = foo(x); в al oop. Инструкции с плавающей запятой имеют большую задержку, чем целое число, поэтому часто это более сложная задача. Доступ к памяти также передается по конвейеру на большинстве процессоров, поэтому циклическая обработка по массиву (адрес для следующей загрузки легко вычисляется) часто намного быстрее, чем просмотр связанного списка (адрес для следующей загрузки недоступен, пока не завершится предыдущая загрузка).

    Очевидно, что производительность может отличаться для разных процессоров; в целом, как правило, версия Intel быстрее для Intel, а версия B быстрее для AMD, но это легко может произойти в небольшом масштабе. Сообщая / записывая номера тестов, всегда обращайте внимание на то, на каком процессоре вы тестировали.

  • Относительно вышеприведенных и приведенных ниже пунктов: вы не можете, например, протестировать оператор * в C. Некоторые варианты использования для него будут компилироваться очень по-другому, например, tmp = foo * i; в al oop может часто превращаться в tmp += foo (уменьшение силы), или если множитель является постоянной степенью 2, компилятор будет просто использовать сдвиг. Один и тот же оператор в исходном коде может компилировать с очень разными инструкциями, в зависимости от окружающего кода.
  • Вам необходимо скомпилировать с включенной оптимизацией , но вам также нужно остановить компилятор от оптимизировать работу или поднять ее из всех oop. Убедитесь, что вы используете результат (например, распечатайте его или сохраните его в volatile), чтобы компилятор должен был его создать. Используйте случайное число или что-то вместо константы времени компиляции для ввода, чтобы ваш компилятор не мог выполнять постоянное распространение для вещей, которые не будут константами в вашем реальном случае использования. В C для этого иногда можно использовать встроенный asm или volatile, например, материал , который этот вопрос задает о . Хороший пакет бенчмаркинга, такой как Google Benchmark , будет включать функции для этого.
  • Если реальный вариант использования функции позволяет встроить ее в вызывающие объекты, где некоторые входные данные являются постоянными, или операции могут быть оптимизированный для другой работы, не очень полезно тестировать его самостоятельно.
  • Большие сложные функции со специальной обработкой для множества особых случаев могут быстро выглядеть в микробенчмарке при повторном их запуске, особенно с один и тот же ввод каждый раз. В реальных случаях использования прогнозирование ветвлений часто не будет выполняться для этой функции с этим вводом. Кроме того, массово развернутый l oop может хорошо выглядеть в микробенчмарке, но в реальной жизни он замедляет все остальное благодаря большой площади кэша команд, что приводит к исключению другого кода.

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

Лакмусовые тесты:

  • Если вы тестируете две функции в одной программе: если изменение порядка тестирования меняет результаты, ваш эталонный тест не будет справедливым. Например, функция A может выглядеть медленно только потому, что вы сначала ее тестируете, с недостаточным прогревом. пример: Почему std :: vector медленнее, чем массив? (это не так, в зависимости от того, какой запуск l oop выполняется первым, он оплачивает все ошибки страницы и ошибки кэширования; 2-й просто увеличивает масштаб, заполняя то же самое память.)

  • Увеличение числа итераций повторения l oop должно линейно увеличивать общее время и не влиять на вычисленное время на вызов. Если нет, то у вас есть незначительные накладные расходы на измерения или ваш код оптимизирован (например, поднят из l oop и выполняется только один раз вместо N раз).

т.е. изменяются параметры теста в качестве проверки работоспособности.


Для C / C ++ см. также Простой для () l oop бенчмарк занимает одинаковое время с любым l oop bound , где я подробно остановился на микробенчмаркинге и использовании volatile или asm, чтобы помешать важной работе оптимизировать с помощью gcc / clang.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...