Измерение интервалов времени в Windows
Иногда нам нужно точно измерить интервалы времени, в течение которых выполняются различные части нашей программы. Если мы программируем под операционную систему Windows, то у нас есть следующие возможности:
- — возвращает время в миллисекундах с момента старта операционной системы («мультимедийный таймер»). Возвращаемое значение (типа 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 QueryPerformanceFrequency(LARGE_INTEGER *lpFrequency); BOOL QueryPerformanceCounter(LARGE_INTEGER *lpPerformanceCount);
Не забывайте, что сам вызов этих функций занимает некоторое время, которое даже может оказаться больше, чем измеряемый интервал времени.
QueryPerformanceFrequency
Аргумент lpFrequency — указатель на переменную типа LARGE_INTEGER, в которую будет записана частота таймера (тиков в секунду).
Возвращаемое значение: если компьютер имеет таймер высокого разрешения, то возвращаемая величина ненулевая. Если же таймер отсутствует либо не поддерживается BIOS, то функция вернёт 0.
QueryPerformanceCounter
Аргумент lpPerformanceCount — указатель на переменную типа LARGE_INTEGER, в которую будет записано значение таймера, «набежавшее» на данный момент (в тиках).
Возвращаемое значение: 0 в случае ошибки, иначе ненулевое значение.
LARGE_INTEGER
Этот тип данных определяется следующим образом:
typedef union _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 <iostream> #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». Таймер можно запустить, поставить на паузу, и сбросить в ноль. Интерфейс класса следующий:
class Timer { //Класс 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 <iostream> #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 <iostream> #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 <iostream> #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?
Prototype implementations for a few simple Windows clocks
Автор: Сухинов Антон
WWW: http://iproc.ru
Адрес статьи на блоге автора: http://iproc.ru/programming/windows-timers/#post-455
Date: 25.10.2011
| Вложение | Размер |
|---|---|
| SAnTimer.zip | 2.28 КБ |
- Войдите или зарегистрируйтесь, чтобы получить возможность отправлять комментарии
- 1035 просмотров



Комментарии
1 комментария(ев)Дата: Втр, 25/10/2011 - 15:24
...однозначно где-то уже это все читал, дежавю.
Вот, TC, скажите, раз вы прорабатывали этот вопрос. Часто в сети возникают вопросы о замере-выдачи микросекундных интервалов под Win-системами. Чтобы вы ответили в данном случае? Просто хочу сравнить со своими доводами.