Скорость работы разных таймеров

Сделал очередной таймер для замера времени выполнения функций. Использовал функцию QueryPerformanceCounter. Она выдает точные результаты и ее легко применять, но оверхед показался слишком высоким.
Использовать timeGetTime для замера мили, микро и даже наносекунд - бессмысленно.
Нашел также реализацию через rdtsc и пробовал ее, но она выдает совершенно неправильные результаты на Vista 64 (ага, опять она). Да и на других системах ненадежна - напишу дальше почему.
Решил написать тестовую программу для замера скорости работы этих функци, чтобы знать их оверхед.

Сама программа - в конце поста.
Тестировал: timeGetTime(), QueryPerformanceCounter и rdtsc (если у вас есть пример других функций - напишите в комментариях, протестирую и их).
Результаты по скорости работы за 1млн вызовов (в миллисекундах):
timeGetTime: 25
RDTSC: 25
QueryPerformanceCounter: 1900
Уже очевидно, что QueryPerformanceCounter добавляет 2мкс оверхеда на каждый свой вызов!!! 500 вызовов - 1мс. А 500 вызовов в секунду для профайлера - это очень мало. Часто бывают тысячи и десятки тысяч вызовов. Так что в случае профайления таких приложений, QueryPerformanceCounter обсолютно неприменим из-за оверхеда. А выглядит очень привлекательно - очень точная функция и очень проста в использовании.
Нет разницы по скорости работы между timeGetTime и RDTSC. Их вызовы занимают наносекунды (25нс), что почти в 1000 быстрее QueryPerformanceCounter и в принципе обе этих функции можно было бы использовать для профайления, если бы не одно но. Точность timeGetTime зависит от системного таймера и обычно составляет 10-15 мс! Минимум - 1мс. Так что если профайлить timeGetTime функции, работающие меньше сотен милисекунд, то вы получите не очень точные результаты.
Остается только RDTSC. Очень быстр, максимальная (наносекундная) точность. Но есть небольшая проблема - как перевести этот счетчик в секунды? Напомню, что команда RDTSC возвращает число тактов процессора с его последнего сброса, а не время. Есть такие реализации, например, где вычисляется счетчик, ставится Sleep(50) и затем опять вычисляется счетчик. И считается, что разница между показателями счетчика - это число тиков за 50мс. Асболютная наивность!
Windows не гарантирует, что поток продолжит выполнение через 50мс. Мало того, в условиях инициализации или boot up (а именно в это время вы будете инициализировать таймер), этот Sleep(50) может остановить ваш поток и на 500 мс. И какова будет точность такого таймера?

Можно поставить Sleep(1000) или Sleep(10000) - это очень сильно повысит точность. Но и остановит ваш процесс инициализации на столько же.

Еще встречал другой вариант. Я считаю, что он неправильный, но его используют. Можно просто воспользоваться функцией QueryPerformanceFrequency для вычисления частоты таймера. И затем воспользоваться такой функцией для перевода в милисекунды:
counter*1000.0f/PerformanceFrequency, где counter - это счетчик из RDTSC, а PerformanceFrequency - это значение из QueryPerformanceFrequency.
Вроде как это должно работать, но, как я уже говорил, на Vista64 не работает.
К тому же у RDTSC есть проблема - он может при следующем вызове возвращать отрицательные значения на некоторых многоядерных компьютерах, то есть ваша функция выполняется как бы отрицательное время :)
Этого можно избежать, выставляя Processor Affinity для каждого потока, но тогда мы ограничиваем систему и получаем меньшую производительность, что плохо.

В общем, мой совет таков:

Если вам нужен быстрый и простой таймер, который вы будете вызывать нечасто, то используйте QueryPerformanceCounter.
Если точность совсем не нужна и счет идет на секунды (как, например, в тестовой программе ниже), то используйте timeGetTime.
Ну а есть вам нужно профайлить тысячи вызовов в секунду с высокой точностью, то используйте RDTSC, но будьте готовы к глюкам, багам и временами неправильным результатам.

Исходники тестовой программы:

 

#include "stdafx.h"
#include <windows.h>
inline void __timeGetTime(DWORD& clocks)
{
clocks = timeGetTime();
}
inline void __QueryPerformanceCounter( __int64& clocks )
{
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&clocks));
}
inline void __RDTSC_ASM( __int64& clocks )
{
LARGE_INTEGER li;
__asm
{
rdtsc
mov li.LowPart, eax
mov li.HighPart, edx
}
clocks = li.QuadPart;
}
int _tmain(int argc, _TCHAR* argv[])
{
__int64 clocks;
DWORD dwClocks;
#define CYCLES_COUNT 1000000
DWORD startTime = timeGetTime();
for(int i = 0; i < CYCLES_COUNT; ++i)
__timeGetTime(dwClocks);
DWORD Time1 = timeGetTime();
for(int i = 0; i < CYCLES_COUNT; ++i)
__RDTSC_ASM(clocks);
DWORD Time2 = timeGetTime();
for(int i = 0; i < CYCLES_COUNT; ++i)
__QueryPerformanceCounter(clocks);
DWORD Time3 = timeGetTime();
printf("timeGetTime: %d\n", (int)(Time1 - startTime));
printf("RDTSC: %d\n", (int)(Time2 - Time1));
printf("QueryPerformanceCounter: %d\n", (int)(Time3 - Time2));
return 0;
}

5 комментариев к Скорость работы разных таймеров

  • airmax

    Для точного измерения времени (правда, в КМ) пользуюсь способом позаимствованным в WinPcap:
    При инициализации драйвера для каждого ядра/процессора вызывается KeQueryPerformanceCounter, и значение запоминается в массиве, где индекс - номер процессора. В последствии начальное значение счётчика можно узнать по номеру процессора на котором выполняется поток.

    Плюсы
    1. Высокая точность получаемых результатов(это как раз и нужно).
    2. Переносимость.
    3. Независимость от кол-ва ядер, ключей загрузки ОС например “/usepmtimer”, итд.
    4. Минимальный оверхед. Поскольку не нужно играться с AffinityMask каждый раз когда нужно узнать время. А номер процессора на котором исполяется поток в текущий момент извлекается из структуры KTHREAD(если я не ошибаюсь) т.е. тоже очень быстро.

    Минусы
    Пока не знаю. Напишите если есть.

    • А код таймера можно увидеть? Тогда можно было бы оверхеды сравнить.
      Как предположение: еще интересно было бы узнать и проверить, как этот счетчик ведет себя в режиме экономного потребления энергии процессором, когда он в неполную силу работает.

  • airmax

    Пробовал вставлять сюда код - редактор режет отступы. Для этого движка должен быть модуль чтобы можно было нормально код оформлять. Всё-таки блог о программировании :)

    Теперь по сути вопроса. Для интервалов больше 16 мс можно использовать KeQueryInterruptTime() или KeQueryTickCount(). Опыт показывает что результат одинаковый(Проверял на х86), хотя MSDN пишет что KeQueryInterruptTime() даёт более точный результат.
    Оверхеад можно сказать нулевой, поскольку значение функций извлекается из системных переменных.
    Если нужна большая точность, без вариантов - KeQueryPerformanceCounter. Да, есть оверхеад. Поскольку при вызове идёт обращение к апаратной части. Сравнивавать не вижу смысла, не с чем.
    Значение возвращаемое KeQueryPerformanceCounter нужно всегда переводить во время, поскольку при вызове также возвращается частота счётчика(Performance Frequency), таким образом при изменениях частоты время для конкретного ядра/процессора всегда одинаково.

    • >>Значение возвращаемое KeQueryPerformanceCounter нужно всегда переводить во время, поскольку при вызове также возвращается частота
      >>счётчика(Performance Frequency), таким образом при изменениях частоты время для конкретного ядра/процессора всегда одинаково.

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

    • >> Для этого движка должен быть модуль чтобы можно было нормально код оформлять. Всё-таки блог о программировании :)

      Ага, надо поискать, да лень :)

Ответить

 

 

 

Вы можете использовать эти HTML тэги

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>