Лучший способ к скорости тестового кода в C++ без профилировщика, или разве не имеет смысла пробовать?

На Так, существует довольно много вопросов о профилировании производительности, но я, кажется, не нахожу целое изображение. Существует довольно много включенных проблем, и большая часть Q & A игнорируют все кроме некоторых за один раз или не выравнивают по ширине их предложения.

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

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

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

#include 
#include 
#include 

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. При считании результатов представленными здесь, Вы думаете, что они точны (я могу уверить Вас, что несколько выполнений дают очень похожие результаты)? Если да, может Вы объяснять как станд.:: реверс добирается, чтобы быть настолько быстрым, рассматривая простоту пользовательской функции. У меня нет исходного кода от vc ++ версия, которую я использовал для этого теста, но здесь являюсь реализацией от GNU. Это сводится к функции iter_swap, который абсолютно непостижим для меня. Это, как также ожидали бы, будет работать дважды с такой скоростью, как та пользовательская функция, и если так, почему?

Рассмотрения:

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

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

Спасибо за любые направления.

обновление

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

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 я нашел, что на окнах можно было использовать фактическое процессорное время (read:your) инструментарий управления Windows канавки процесса. Это определенно выглядит более интересным, чем счетчик высокой точности.

8
задан Community 23 May 2017 в 12:30
поделиться

8 ответов

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

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

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

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

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

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

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

4
ответ дан 5 December 2019 в 15:19
поделиться

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

2
ответ дан 5 December 2019 в 15:19
поделиться

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

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

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

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

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

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

2
ответ дан 5 December 2019 в 15:19
поделиться

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

2
ответ дан 5 December 2019 в 15:19
поделиться

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

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

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

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

1
ответ дан 5 December 2019 в 15:19
поделиться

(Этот ответ специфичен для 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
ответ дан 5 December 2019 в 15:19
поделиться

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

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

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

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

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

1
ответ дан 5 December 2019 в 15:19
поделиться

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

Кроме того, вы должны использовать STD :: своп в первую очередь, что полная аннулирует вся эта бессмысленная погоня.

-1 за бессмысленность.

-3
ответ дан 5 December 2019 в 15:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: