Лучший способ проверить скорость кода в C ++ без профилировщика, или нет смысла пробовать? - PullRequest
8 голосов
/ 27 июня 2010

На SO есть довольно много вопросов о профилировании производительности, но я не вижу полной картины. Есть довольно много проблем, и большинство вопросов и ответов игнорируют все, кроме нескольких, или не оправдывают свои предложения.

Что мне интересно. Если у меня есть две функции, которые делают одно и то же, и мне интересно узнать разницу в скорости, имеет ли смысл тестировать это без внешних инструментов, с таймерами, или это скомпилировано в тестировании, сильно повлияет на результаты?

Я спрашиваю об этом, потому что, если это разумно, как программисту на C ++, я хочу знать, как это лучше всего сделать, поскольку они намного проще, чем использование внешних инструментов. Если это имеет смысл, давайте продолжим со всеми возможными ловушками:

Рассмотрим этот пример. Следующий код показывает 2 способа сделать то же самое:

#include <algorithm>
#include <ctime>
#include <iostream>

typedef unsigned char byte;

inline
void
swapBytes( void* in, size_t n )
{
   for( size_t lo=0, hi=n-1; hi>lo; ++lo, --hi )

      in[lo] ^= in[hi]
   ,  in[hi] ^= in[lo]
   ,  in[lo] ^= in[hi] ;
}

int
main()
{
         byte    arr[9]     = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' };
   const int     iterations = 100000000;
         clock_t begin      = clock();

   for( int i=iterations; i!=0; --i ) 

      swapBytes( arr, 8 );

   clock_t middle = clock();

   for( int i=iterations; i!=0; --i ) 

      std::reverse( arr, arr+8 );

   clock_t end = clock();

   double secSwap = (double) ( middle-begin ) / CLOCKS_PER_SEC;
   double secReve = (double) ( end-middle   ) / CLOCKS_PER_SEC;


   std::cout << "swapBytes,    for:    "   << iterations << " times takes: " << middle-begin
             << " clock ticks, which is: " << secSwap    << "sec."           << std::endl;

   std::cout << "std::reverse, for:    "   << iterations << " times takes: " << end-middle
             << " clock ticks, which is: " << secReve    << "sec."           << std::endl;

   std::cin.get();
   return 0;
}

// Output:

// Release:
//  swapBytes,    for: 100000000 times takes: 3000 clock ticks, which is: 3sec.
//  std::reverse, for: 100000000 times takes: 1437 clock ticks, which is: 1.437sec.

// Debug:
//  swapBytes,    for: 10000000 times takes: 1781  clock ticks, which is: 1.781sec.
//  std::reverse, for: 10000000 times takes: 12781 clock ticks, which is: 12.781sec.

Выпуски:

  1. Какие таймеры использовать и как фактически использовать процессорное время для рассматриваемого кода?
  2. Каковы эффекты оптимизации компилятора (поскольку эти функции просто меняют байты назад и вперед, очевидно, что наиболее эффективная вещь - вообще ничего не делать)?
  3. Учитывая результаты, представленные здесь, считаете ли вы, что они точны (могу вас заверить, что многократные прогоны дают очень похожие результаты)? Если да, можете ли вы объяснить, как std :: reverse становится таким быстрым, учитывая простоту пользовательской функции. У меня нет исходного кода из версии vc ++, которую я использовал для этого теста, но здесь - реализация из GNU. Это сводится к функции iter_swap , которая для меня совершенно непонятна. Можно ли ожидать, что это будет выполняться вдвое быстрее, чем эта пользовательская функция, и если да, то почему?

Размышления:

  1. Похоже, предлагаются два высокоточных таймера: clock () и QueryPerformanceCounter (для окон). Очевидно, что мы хотели бы измерять время процессора нашего кода, а не реальное время, но, насколько я понимаю, эти функции не предоставляют такой функциональности, поэтому другие процессы в системе будут мешать измерениям. Эта страница в библиотеке gnu c, кажется, противоречит этому, но когда я ставлю точку останова в vc ++, отлаживаемый процесс получает много тактов, даже если он был приостановлен (я не тестировал под gnu). Я пропускаю альтернативные счетчики для этого или нам нужны хотя бы специальные библиотеки или классы для этого? Если нет, достаточно ли хороших часов в этом примере или будет ли причина использовать QueryPerformanceCounter?

  2. Что мы можем знать наверняка без средств отладки, разборки и профилирования? Что-нибудь на самом деле происходит? Является ли вызов функции встроенным или нет? При регистрации в отладчике байты действительно меняются местами, но я предпочел бы знать из теории почему, чем из тестирования.

Спасибо за любые указания.

обновление

Благодаря подсказке от tojas функция swapBytes теперь работает так же быстро, как и std :: reverse. Я не смог понять, что временная копия в случае байта должна быть только регистром, и поэтому очень быстрая. Элегантность может ослепить вас.

inline
void
swapBytes( byte* in, size_t n )
{
   byte t;

   for( int i=0; i<7-i; ++i )
    {
        t       = in[i];
        in[i]   = in[7-i];
        in[7-i] = t;
    }
}

Благодаря совету от ChrisW Я обнаружил, что в Windows вы можете получить фактическое время ЦП, затраченное на процесс (читай: ваш) Инструментарий управления Windows . Это определенно выглядит интереснее, чем высокоточный счетчик.

Ответы [ 8 ]

4 голосов
/ 27 июня 2010

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

Я делаю две вещи, чтобы гарантировать, что время настенных часов и время процессора примерно одинаковы:

  • Тестирование в течение значительного промежутка времени, то есть нескольких секунд (например, путем тестирования цикла, состоящего из многих тысяч итераций)

  • Проверка, когда машина более или менее относительно простаивает, за исключением того, что я тестирую.

В качестве альтернативы, если вы хотите измерить только / точнее время ЦП на поток, это доступно как счетчик производительности (см., Например, perfmon.exe).

Что мы можем знать наверняка без инструментов отладки, разборки и профилирования?

Почти ничего (кроме того, что ввод / вывод имеет тенденцию быть относительно медленным).

2 голосов
/ 28 июня 2010

(Этот ответ относится только к Windows XP и 32-битному компилятору VC ++.)

Самая простая вещь для синхронизации маленьких битов кода - это счетчик меток времени ЦП.Это 64-разрядное значение, подсчитывающее количество циклов ЦП, которые выполняются до сих пор, что примерно такое же хорошее разрешение, как и вы.Фактические числа, которые вы получаете, не особенно полезны, но если вы усредняете несколько прогонов различных конкурирующих подходов, то вы можете сравнить их таким образом.Результаты немного шумные, но все еще действительны для целей сравнения.

Чтобы прочитать счетчик меток времени, используйте код, подобный следующему:

LARGE_INTEGER tsc;
__asm {
    cpuid
    rdtsc
    mov tsc.LowPart,eax
    mov tsc.HighPart,edx
}

(инструкция cpuidчтобы убедиться, что нет никаких неполных инструкций, ожидающих завершения.)

Об этом подходе стоит отметить четыре вещи.

Во-первых, из-за встроенного языка ассемблера он выиграл 'не работает на компиляторе MS x64.(Вам нужно будет создать файл .ASM с функцией в нем. Упражнение для читателя; я не знаю деталей.)

Во-вторых, чтобы избежать проблем с синхронизацией счетчиков цикловв разных ядрах / потоках / что у вас есть, вы можете счесть необходимым установить сродство вашего процесса так, чтобы он работал только на одном конкретном исполнительном модуле.(Опять же ... вы не можете.)

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

Наконец, результаты довольно шумные.Счетчики циклов подсчитывают циклы, потраченные на все, включая ожидание кэшей, время, потраченное на запуск других процессов, время, потраченное на саму ОС, и т. Д. К сожалению, невозможно (по крайней мере, под Windows) рассчитать время только на ваш процесс.Итак, я предлагаю запускать тестируемый код много раз (несколько десятков тысяч) и вычислять среднее значение.Это не очень хитро, но, по-видимому, оно принесло мне полезные результаты.

2 голосов
/ 27 июня 2010

Используйте QueryPerformanceCounter в Windows, если вам требуется синхронизация с высоким разрешением. Точность счетчика зависит от процессора, но может увеличиваться до тактового импульса. Тем не менее, профилирование в реальных операциях всегда лучше.

2 голосов
/ 27 июня 2010

Можно ли с уверенностью сказать, что вы задаете два вопроса?

  • Какой из них быстрее и на сколько?

  • Ипочему он быстрее?

Для начала вам не нужны высокоточные таймеры.Все, что вам нужно сделать, это запустить их «достаточно долго» и измерять с таймерами низкой точности.(Я старомоден, у моих наручных часов есть функция секундомера, и она достаточно хороша).

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

Думайте просто.Производительность не сложная тема.Обычно люди пытаются найти проблемы , для которых это простой подход .

2 голосов
/ 27 июня 2010

Чтобы ответить на ваш главный вопрос, его «обратный» алгоритм просто меняет элементы в массиве и не работает с элементами массива.

1 голос
/ 27 июня 2010

Я полагаю, что любой, кто достаточно компетентен, чтобы ответить на все ваши вопросы, будет слишком занят, чтобы отвечать на все ваши вопросы.На практике, вероятно, более эффективно задавать единые, четко определенные вопросы.Таким образом, вы можете надеяться получить четкие ответы, которые сможете собрать и быть на пути к мудрости.

Итак, в любом случае, возможно, я смогу ответить на ваш вопрос о том, какие часы использовать в Windows.

clock () не считается часами высокой точности.Если вы посмотрите на значение CLOCKS_PER_SEC, вы увидите, что оно имеет разрешение 1 миллисекунду.Этого достаточно, если вы рассчитываете очень длинные подпрограммы или цикл с 10000 итерациями.Как вы указали, если вы попытаетесь повторить простой метод 10000 раз, чтобы получить время, которое можно измерить с помощью clock (), компилятор может вмешаться и оптимизировать все это.

Так что, действительно, единственными часами, которые нужно использовать, является QueryPerformanceCounter ()

1 голос
/ 27 июня 2010

Есть ли что-то, что вы имеете против профилировщиков?Они помогают тонну.Поскольку вы работаете на WinXP, вам действительно стоит попробовать vtune.Попробуйте выполнить тест выборки графа вызовов и посмотрите на собственное время и общее время вызываемых функций.Нет лучшего способа настроить вашу программу так, чтобы она была максимально быстрой, не будучи гением сборки (и действительно исключительной).

У некоторых людей, похоже, есть аллергия на профилировщики.Раньше я был одним из них и думал, что лучше знаю, где находятся мои горячие точки.Я часто был прав насчет очевидной неэффективности алгоритмов, но практически всегда ошибался в отношении большего количества случаев микрооптимизации.Простое переписывание функции без изменения какой-либо логики (например, переупорядочивание, помещение исключительного регистра кода в отдельную, не встроенную функцию и т. Д.) Может сделать функции в десятки раз быстрее, и даже лучшие эксперты по разборке обычно не могут предсказать, чтобез профилировщика.

Что касается использования только упрощенных временных испытаний, они чрезвычайно проблематичны.Этот текущий тест не так уж и плох, но очень распространенной ошибкой является написание временных тестов таким образом, чтобы оптимизатор оптимизировал мертвый код и заканчивал тем, что тестировал время, необходимое для того, чтобы выполнить практически ничего или вообще ничего.У вас должны быть некоторые знания для интерпретации разборки, чтобы убедиться, что компилятор этого не делает.

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

В зависимости от реальных тестов времени немного лучше;что-то ближе к тому, что ваше приложение будет делать на высоком уровне.Он не даст вам конкретной информации о том, что занимает какое количество времени, но это именно то, для чего предназначен профилировщик.

0 голосов
/ 27 июня 2010

Wha?Как измерить скорость без профилировщика? Сам акт измерения скорости - это профилирование! Вопрос сводится к тому, "как я могу написать свой собственный профилировщик?"И ответ однозначен: «не».

Кроме того, вы должны использовать std::swap во-первых, что делает недействительным все это бессмысленное стремление.

-1 для бессмысленности.

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