Измерение интервалов времени в Windows
Описанные здесь функции называют «таймерами Windows».
Иногда нам нужно точно измерить интервалы времени, в течение которых выполняются различные части нашей программы. Если мы программируем под операционную систему Windows, то у нас есть следующие возможности:
- timeGetTime() — возвращает время в миллисекундах с момента старта операционной системы («мультимедийный таймер»). Возвращаемое значение (типа
DWORD) обнуляется каждые 49.71 дней. Не забудьте это учесть при измерении времени. Точность: от 5 до 1 миллисекунды. Если вы хотите повысить точность до 1 миллисекунды, то можете поместить измерение времени между вызовамиtimeBeginPeriod(1);иtimeEndPeriod(1);. Учтите, что вызовtimeBeginPeriod()может временно повысить частоту мультимедийного таймера во всей системе, тем самым снизив её быстродействие. - GetTickCount() — возвращает время в миллисекундах с момента старта операционной системы (обнуляется каждые 49.71 дней). Очень быстрая функция, так как просто возвращает значение соответствующего счётчика. Однако точность её низка: обычно 55 миллисекунд в Win 9X и 10 миллисекунд в Win NT. Низкая точность вызвана тем, что для увеличения счётчика используются прерывания, генерируемые часами реального времени компьютера. Для генерации сообщений
WM_TIMER, приходящих вашему приложению, тоже используется этот счётчик. Вы можете повысить точность этой функции до 1 миллисекунды, если являетесь счастливым обладателем Win 2000/XP. Для этого нужно добавить команду/USEPMTIMERвboot.ini(в Win Vista это тоже можно сделать, но с использованием BCDedit). Это переключит HAL вашей системы на генерацию прерываний таймера с использованием ACPI Power Management Timer (а не часов реального времени). Вы делаете это на свой страх и риск. Будьте готовы к тому, что функционирование системы может измениться. - GetSystemTime() — текущее время (год, месяц, день, часы, минуты, секунды, миллисекунды). Точность та же, что и у
GetTickCount(), так как используются те же часы реального времени. Точность тоже можно повысить с помощью ключа/USEPMTIMER. Использовать эту функцию для вычисления интервалов времени неудобно, так как секунды, минуты, часы, и тому подобные единицы сложно вычитать друг из друга. К тому же системное время в любой момент может измениться (синхронизация с сервером времени, либо переход на летнее время и обратно). - Машинная инструкция RDTSC — читает из процессорного счётчика число тактов, прошедшее с момента запуска процессора. Самый точный счётчик из доступных. Однако имеются следующие проблемы:
- Частота процессора неизвестна. Более того, она может меняться со временем в зависимости от режима энергосбережения.
- В многоядерных процессорах и многопроцессорных системах счётчик свой у каждого ядра́/процессора. И эти счётчики не синхронизированы, так как я́дра/процессоры могут быть запущены в разные моменты времени, и даже могут быть на некоторое время приостановлены. Поэтому показание этого счётчика будет «прыгать» при переходе потока с одного ядра на другое.
- QueryPerformanceCounter() — «таймер высокого разрешения». Введён фирмой Microsoft, чтобы раз и навсегда поставить точку в проблемах измерения времени. Частота этого таймера (1 МГц и выше) не меняется во время работы системы. Частоту можно узнать с помощью функции
QueryPerformanceFrequency(). Для каждой системы Windows сама определяет, с помощью каких устройств реализовать этот таймер. Похоже, это самый лучший способ измерения времени из всех, поэтому о нём и поговорим.
Для работы с таймером высокого разрешения имеются две API-функции:
BOOL QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);
Не забывайте, что сам вызов этих функций занимает некоторое время, которое даже может оказаться больше, чем измеряемый интервал времени.
QueryPerformanceFrequency
Аргумент lpFrequency — указатель на переменную типа LARGE_INTEGER, в которую будет записана частота таймера (тиков в секунду).
Возвращаемое значение: если компьютер имеет таймер высокого разрешения, то возвращаемая величина ненулевая. Если же таймер отсутствует либо не поддерживается BIOS, то функция вернёт 0.
QueryPerformanceCounter
Аргумент lpPerformanceCount — указатель на переменную типа LARGE_INTEGER, в которую будет записано значение таймера, «набежавшее» на данный момент (в тиках).
Возвращаемое значение: 0 в случае ошибки, иначе ненулевое значение.
LARGE_INTEGER
Этот тип данных определяется следующим образом:
struct {
DWORD LowPart;
LONG HighPart;
};
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER;
Мы видим, что здесь 3 элемента, находящиеся в одном месте оперативной памяти: безымянная структура, хранящая две 32-х битных «половинки» числа, структура u с тем же содержимым, и 64-х битное число (__int64). Всё так сложно потому, что «некоторые системы и компиляторы могут не поддерживать 64-х битные числа». Мы будем пользоваться только QuadPart.
Пример
Описанные выше функции можно использовать примерно так:
#include <windows.h>
using namespace std;
int main(int argc, char **argv)
{
LARGE_INTEGER timerFrequency, timerStart, timerStop;
QueryPerformanceFrequency(&timerFrequency);
QueryPerformanceCounter(&timerStart);
//
//Здесь код, время работы которого замеряем
//
QueryPerformanceCounter(&timerStop);
double const t( static_cast<double>( timerStop.QuadPart -
timerStart.QuadPart ) / timerFrequency.QuadPart );
cout << "Time is " << t << " seconds." << endl;
return 0;
}
Не всё так хорошо!
Так как в разных системах таймер высокого разрешения реализуется с помощью разных устройств, то будьте готовы к проблемам, если этот таймер работает с помощью счётчика тактов процессора:
- Встречаются системы, в которых таймер высокого разрешения меняет скорость работы при переключении частоты процессора.
- Бывает, что таймер «прыгает туда-сюда» при переходе процесса с одного ядра на другой.
Но, в отличие от счётчика тактов, для которого такие «аномалии» — норма, для таймера высокого разрешения это — глюки. И производители процессоров вместе с Microsoft стараются их исправлять. Например: http://www.reghardware.co.uk/2006/07/04/amd_dual-core_tweak_tool/.
Если же вам нужна высокая надёжность, то от этого таймера придётся отказаться. Рекомендую в этом случае обратить свой взор на timeGetTime() в связке с timeBeginPeriod() и timeEndPeriod().
Библиотека для работы с таймером
Видно, что для измерения интервалов времени в программе требуется заводить дополнительные переменные, писать лишний и некрасивый код. Поэтому я написал простейшую библиотеку для работы с таймером, которую вы можете скачать отсюда: SAnTimer.h (лицензия LGPL).
Библиотека предоставляет программисту класс timer::Timer, который, судя по названию, имитирует таймер. Вы можете создать в программе несколько таймеров. Каждый таймер в момент создания имеет показание «0». Таймер можно запустить, поставить на паузу, и сбросить в ноль. Интерфейс класса следующий:
{ //Класс Timer имитирует некий счётчик времени,
// который можно запустить и остановить
public:
typedef long long Time;
private:
//Если true, то таймер запущен. Иначе -- нет
bool state;
//Сколько времени набежало за все предыдущие интервалы
Time timeTotal;
//Момент начала последнего, ещё незавершённого интервала (если запущен)
Time timeLastStart;
//Узнать текущее значение времени в "тиках"
static inline Time now(void);
public:
//Конструктор по-умолчанию
inline Timer(bool start = false);
//Конструктор копии
inline Timer(Timer const &T);
//Деструктор
inline ~Timer(void);
//Оператор присваивания
inline Timer &operator=(Timer const &T);
//Узнать текущее значение таймера в секундах
inline double get(void) const;
//Узнать текущее значение таймера в секундах
inline operator double(void) const;
//Узнать текущее значение таймера в "тиках" (максимальная точность)
inline Time getTickCount(void) const;
//Узнать частоту таймера (число "тиков" в секунду)
static inline Frequency::Time getFrequency(void);
//Запустить таймер
inline void start(bool reset = false);
//Запустить таймер и узнать значение на момент запуска
inline double startGet(void);
//Пауза таймера
inline void pause(void);
//Пауза таймера и возврат значения
inline double pauseGet(void);
//Остановка таймера. Отличается от паузы обнулением счётчика
inline void stop(void);
//Остановка таймера. Возвращается значение на момент остановки, ДО обнуления
inline double stopGet(void);
};
Для того, чтобы создать таймер в программе, нужно создать объект класса timer::Timer. Необязательный аргумент конструктора указывает на то, нужно ли запускать таймер сразу после создания, либо он будет запущен потом с помощью вызова соответствующей функции.
Назначение других методов класса:
get(): узнать набежавшее значение таймера (в секундах).getTickCount(): узнать набежавшее значение таймера (в тиках).getFrequency(): узнать частоту таймера (тиков в секунду).start(): запустить таймер.startGet(): запустить таймер и узнать значение таймера на момент запуска.pause(): приостановить таймер.pauseGet(): приостановить таймер и узнать его значение (в секундах).stop(): остановить таймер и сбросить его показания в «0».stopGet(): остановить таймер, узнать значение таймера на момент остановки, сбросить его показания.
Описанный выше пример теперь можно переписать следующим образом:
#include "SAnTimer.h" //Подключаем заголовочный файл библиотеки
using namespace std;
int main(int argc, char **argv)
{
timer::Timer t(true); //Создаём таймер и сразу его запускаем
//
//Здесь код, время работы которого замеряем
//
//Таймер останавливается, и его значение сразу выводится:
cout << "Time is " << t.stopGet() << " seconds." << endl;
return 0;
}
Видно, что код стал гораздо проще. Но для более точного измерения времени следует учесть, что функция вывода текста работает очень медленно, и поэтому нужно остановить таймер до начала вывода текста:
#include "SAnTimer.h" //Подключаем заголовочный файл библиотеки
using namespace std;
int main(int argc, char **argv)
{
timer::Timer t(true); //Создаём таймер и сразу его запускаем
//
//Здесь код, время работы которого замеряем
//
//Таймер останавливается, а затем его значение выводится:
t.pause();
cout << "Time is " << t << " seconds." << endl;
return 0;
}
Функция паузы таймера может быть использована, например, чтобы измерить суммарное время, проведённое программой в какой-либо функции:
#include "SAnTimer.h" //Подключаем заголовочный файл библиотеки
using namespace std;
timer::Timer t; //Создаём таймер
void measuredFunc(void)
{
t.start(); //Запускаем таймер на время работы функции
//
//Здесь код функции
//
t.pause(); //Приостанавливаем таймер
}
int main(int argc, char **argv)
{
//Вызываем несколько раз функцию:
for(int i(0); i<10000; ++i)
{
measuredFunc();
//
//Здесь выполняем какие-нибудь другие действия
//
}
//Выводим время на экран:
cout << "Time is " << t << " seconds." << endl;
return 0;
}
Библиотека реализована оптимально. При компиляции с включённой подстановкой inline-функций замер времени будет идти не медленнее, чем если бы он был написан путём вызова функций GetPerformanceCounter(). Более того, вызов GetPerformanceFrequency() осуществляется один раз в самом начале работы программы, а затем полученное значение используется всеми созданными таймерами.
Рекомендую также почитать
Beware of QueryPerformanceCounter()
What the /usepmtimer switch does when placed in a Microsoft Windows boot.ini?
Шесть отзывов на запись «Измерение интервалов времени в Windows»
Автор: ctapmex. Дата: 22-го января 2010 г. Время: 15:29.
Спасибо за статью.
единственное я её долго искал, нашел только через QueryPerformanceCounter. добавь для неё теги , что то типа «измерение скорости кода».
Автор: Вася. Дата: 1-го февраля 2010 г. Время: 08:52.
а у меня касяк полный:
// var timer_frequency: Int64;
QueryPerformanceFrequency(timer_frequency);
ShowMessage(IntToStr(timer_frequency));
показывает всегда 2833030000…и как это в частоту пересчитать?
а время выполнения кода что я считаю получается девять десятитысячных…%(
Автор: Антон. Дата: 1-го февраля 2010 г. Время: 08:59.
2833030000 — это и есть частота. 3 гигагерца примерно.
Автор: Вася. Дата: 1-го февраля 2010 г. Время: 09:10.
уж не вы ли это?о_О
вы с Мастеров/DeskSoft?х_Х
значит девять десятитысячных — нормальный результат?
Автор: Антон. Дата: 1-го февраля 2010 г. Время: 09:35.
Нет, это не я.
Автор: Александр. Дата: 25-го сентября 2010 г. Время: 00:25.
Очень полезная статья. Спасибо.
Правда, хотелось бы реабилитировать функцию GetSystemTime(). Точность ее довольно высока (определяется часами RTC на материнке). Синхронизация обычно раз в две недели. От летнего времени не зависит, выражается в UTC (по Гринвичу). Летнее время влияет на GetLocalTime(). Дает отсчет в 100-наносекундных интервалах, начиная с 1 января 1601 года. Кстати, это был понедельник. Если не нравится структура SYSTEMTIME, переводим в FILETIME функцией SystemTimeToFileTime(). А это тот же LARGE_INTEGER, которым вы пользуетесь. Лично мне нравится.