Итак, задача: сделать простой и переносимый класс C++ для работы с интервалами времени в миллисекундах. Должно работать в Windows и UNIX.Я придумал вот такой интерфейс для класса:
class PreciseTimer {
public:
// Тип для работы с тиками таймера. По сути это целое в 64 бита,
// но конкретное имя рабочего типа будет зависеть от платформы.
typedef s_int_64 Counter;
// Функция получение текущего значения миллисекундного таймера.
// Само по себе это число особого смысла не имеет, так как оно
// ни к чему не привязано, а вот разница двух таких чисел как
// раз используется для замеров интервалов времени.
// Функция возвращает 0 под Windows, если не удается получить
// значение системного таймера.
Counter millisec();
// Задержка на указанное число миллисекунд. Необходимо учитывать,
// что в UNIX системах данная функция может быть прервана
// системым сигналом (signal). В этом случае задержка может быть
// меньше, чем ожидалось.
static void sleepMs(int ms);
// Функция "отметки" текущего момента времени.
// Добавляет текущее время в очередь отметок.
void mark();
// Функция получения времени, прошедшего с последней отметки
// в функции mark(). Последняя отметка вынимается из очереди
// и вычитается из текущего значения таймера. Эта разница и
// есть результат функции. Если очередь отметок пуста (никто
// не вызывал mark() до этого), то возвращается -1.
Counter release();
// Парные вызовы mark()/release() могут быть вложенными.
//
// Примерная техника работы с классом:
// ...
// PreTimer timer;
// ...
// timer.mark();
// ...что-то делаем тут (1)
// timer.mark();
// ...что-то еще делаем тут (2)
// /* получаем продолжительность дела (2) */
// t1 = timer.release();
// /* получаем суммарную продолжительность дел (1) и (2) */
// t2 = timer.release();
// /* А t3 уже равно -1, так как очередь пуста, так как этот
// * вызов release() третий в счету, а вызовов mark() было
// * всего два */
// t3 = timer.release();
}
Реалиазация вышла довольно простая. Всего один файл pretimer.h (без .cpp) без внешних нестандартных зависимостей.Файл pretimer.h:
#ifndef _EXT_PRETIMER_H
#define _EXT_PRETIMER_H
#include <stack>
#ifdef WIN32
#include <windows.h>
#else
#include <sys/time.h>
#include <unistd.h> // usleep()
#endif
// namespace, традиционно, с именем "ext", так что измените под ваши
// привычки именования, если надо.
namespace ext {
class PreciseTimer {
public:
#ifdef WIN32
// Тип int64 для Windows
typedef LONGLONG Counter;
#else
// Тип int64 для UNIX
typedef long long Counter;
#endif
PreciseTimer();
Counter millisec();
void mark();
Counter release();
static void sleepMs(int ms);
private:
// Тип стека для хранения отметок времени.
typedef std::stack< Counter > Counters;
// Стек для хранения отметок времени.
Counters __counters;
#ifdef WIN32
// Для Windows надо хранить системную частоту таймера.
LARGE_INTEGER __freq;
#endif
};
void PreciseTimer::mark() {
__counters.push(millisec());
}
PreciseTimer::Counter PreciseTimer::release() {
if( __counters.empty() ) return -1;
Counter val = millisec() - __counters.top();
__counters.pop();
return val;
}
#ifdef WIN32
PreciseTimer::PreciseTimer() {
// Для Windows в конструкторе получаем системную частоту таймера
// (количество тиков в секунду).
if (!QueryPerformanceFrequency(&__freq))
__freq.QuadPart = 0;
}
PreciseTimer::Counter PreciseTimer::millisec() {
LARGE_INTEGER current;
if (__freq.QuadPart == 0 || !QueryPerformanceCounter(¤t))
return 0;
// Пересчитываем количество системных тиков в миллисекунды.
return current.QuadPart / (__freq.QuadPart / 1000);
}
void PreciseTimer::sleepMs(int ms) {
Sleep(ms);
}
#else // WIN32
PreciseTimer::PreciseTimer() {}
PreciseTimer::Counter PreciseTimer::millisec() {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
void PreciseTimer::sleepMs(int ms) {
usleep(ms * 1000);
}
#endif // WIN32
} // ext
#endif // _EXT_PRETIMER_H
Итак, класс готов, но надо попробовать его в работе. Я, как сугубый апологет unit-тестирования, напишу тесты. Для их компиляции вам потребуется библиотека Google Test Framework. Вы можете взять оригинал с официального сайта, а можете для простоты воспользоваться моей версией, упакованной в два компактных файла gtest-all.cc и gtest.h. Я уже писал про это в рассказе про unit-тестирование. Там я подробно описал, как подготовить Google Test к удобной работе.Итак, тесты.
Файл pretimer_unittest.cpp:
#include "gtest/gtest.h"
#include <cstdlib>
// Подключаем наш класс
#include "pretimer.h"
// Простой тест, для Windows, в основном, для проверки
// доступности системного таймера.
TEST(PreciseTimer, PreciseSystemTimerAvailability) {
ext::PreciseTimer timer;
// Если метод millisec() возвращает 0, значит недоступен
// системный таймер.
EXPECT_NE(0, timer.millisec()) << "Недоступен системный таймер";
}
// Тестирует "точность" измерений.
TEST(PreciseTimer, MeasurementAccuracy) {
// Тестируем на задержке в 100 миллисекунд.
const int delay_ms = 100;
// Зададим наше допустимое отклонение в 10% (10 миллисекунд).
// Функция задержки msleep() тоже неидеальна и привносит
// какую-то погрешность помимо наших измерений.
const int allowed_delta_ms = 10;
// Создаем таймер
ext::PreciseTimer timer;
// Замечаем время
timer.mark();
// Ждем 100 миллисекунд
msleep(delay_ms);
// Вычисляем модуль разницы между эталоном в 100 миллисекунд
// и измеренным нами интервалом через mark()/release()
int delta = std::abs(static_cast<int>(delay_ms - timer.release()));
// Если отклонение более 10 миллисекунд - ошибка.
EXPECT_TRUE(delta <= allowed_delta_ms)
<< "Слишком большое отклонение " << delta << ", превышающее " << allowed_delta_ms;
}
// Тестируем очередь замеров
TEST(PreciseTimer, Queue) {
// Создаем таймер
ext::PreciseTimer timer;
// Делаем замер номер 1
timer.mark();
// Делаем замер номер 2
timer.mark();
// Получаем текущее значение таймера
ext::PreciseTimer::Counter a = timer.release();
// Ждем 100 миллисекунд
monitor::PreciseTimer::sleepMs(100);
// Проверяем, что значение таймера до задежки
// меньше, чем после. Этим мы проверили, что
// очередь замеров работает, так как получили
// корректное значение второго в очереди замера.
EXPECT_LT(a, timer.release());
}
// Проверка пустой очередь замеров
TEST(PreciseTimer, EmptyQueue) {
ext::PreciseTimer timer;
// Если очередь замеров пуста, метод release() должен
// возвращать -1.
EXPECT_EQ(-1, timer.release());
}
Я потратил на этот класс часа четыре неторопливой работы, а на написание тестов всего полчаса, но эти полчаса будут мне служить верой и правдой еще очень долго.Забавно, что когда я запускал эти тесты как-то на Windows под виртуальной машиной, то тест
MeasurementAccuracy
давал сбой! Видимо виртуальная машина как-то неправильно эмулировала работу таймеров, и замер делался совершенно неправильно. А вот теперь если представить - как бы я искал этот баг вручную по всей боевой программе, а? Кто ж мог предположить, что в виртуальной среде что-то можно пойти не так с таймерами. Снова повторю - unit тестирование forever!В завершении, нам нужна главная программа для запуска тестов:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
// Инициализируем библиотеку
testing::InitGoogleTest(&argc, argv);
// Запускаем все тесты, прилинкованные к проекту
return RUN_ALL_TESTS();
}
Компилируем:Visual Studio:
cl /EHsc /I. /DWIN32 /Fepretimer_unittest.exe runner.cpp pretimer_unittest.cpp gtest-all.cc
UNIX:g++ -I. -o pretimer_unittest runner.cpp pretimer_unittest.cpp gtest-all.cc
Запускаем pretimer_unittest и получаем:[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 4 tests from PreciseTimer
[ RUN ] PreciseTimer.PreciseSystemTimerAvailability
[ OK ] PreciseTimer.PreciseSystemTimerAvailability
[ RUN ] PreciseTimer.MeasurementAccuracy
[ OK ] PreciseTimer.MeasurementAccuracy
[ RUN ] PreciseTimer.Queue
[ OK ] PreciseTimer.Queue
[ RUN ] PreciseTimer.EmptyQueue
[ OK ] PreciseTimer.EmptyQueue
[----------] Global test environment tear-down
[==========] 4 tests from 1 test case ran.
[ PASSED ] 4 tests.
Ура! Все работает. Доказано тестами. При использовании данного класса у себя в проекте не забудьте добавить pretimer_unittest.cpp в набор ваших прочих unit тестов. Этим вы избавитесь от множества сюрпризов.Приобщайтесь к unit-тестированию, и программируйте правильно!
Другие посты по теме:
Спасибо, пригодился класс для программы, тестирующей алгоритмы сортировки.
ОтветитьУдалитьВзял с вас привычку любить gTest
Здорово! Значит главный message был доставлен по назначению.
ОтветитьУдалитьА как насчёт SetThreadAffinityMask?
ОтветитьУдалитьНа многопроцессорных и многоядерных системах как это будет выполняться?
Вот кусок из таймера OGRE:
unsigned long Timer::getMilliseconds()
{
LARGE_INTEGER curTime;
HANDLE thread = GetCurrentThread();
// Set affinity to the first core
DWORD oldMask = SetThreadAffinityMask(thread, mTimerMask);
// Query the timer
QueryPerformanceCounter(&curTime);
// Reset affinity
SetThreadAffinityMask(thread, oldMask);
NULL: Пока я не видел сбоя тестов на многопроцессорных и многоядерных системах. Надо будет погонять еще.
ОтветитьУдалитьСпасибо за замечание.
Фу, переглючило слегка. У вас же он кроссплатформенный. Извиняюсь.
ОтветитьУдалитьНо то, что конкретно к винде относится.
Сбоя может и не быть, но неточности.
Как насчёт Unix не знаю.
Как меня запарили эти таймеры!
Ещё, товарищи, написавшие OGRE, в том месте, где они выдают значение миллисекунд, делают какую-то хитрую проверку. Чтобы учесть некие "утечки производительности". Пока не разобрался что такое.
newTicks - число миллисекунд, полученное, почти аналогично вашей функции PreciseTimer::millisec().
Это при сбросе-запуске таймера:
// Query the timer
QueryPerformanceCounter(&mStartTime);
mStartTick = GetTickCount();
// detect and compensate for performance counter leaps
// (surprisingly common, see Microsoft KB: Q274323)
unsigned long check = GetTickCount() - mStartTick;
signed long msecOff = (signed long)(newTicks - check);
if (msecOff < -100 || msecOff > 100)
{
// We must keep the timer running forward :)
LONGLONG adjust = (std::min)(msecOff * mFrequency.QuadPart / 1000, newTime - mLastTime);
mStartTime.QuadPart += adjust;
newTime -= adjust;
// Re-calculate milliseconds
newTicks = (unsigned long) (1000 * newTime / mFrequency.QuadPart);
}
Да, и насчёт Sleep.
Из Рихтера:
'Система прекращает выделять потоку процессорное время на период, примерно равный заданному. Все верно: если Вы укажете остановить поток на 100 мс, приблизительно на столько он и "заснет", хотя не исключено, что его сон продлится на несколько секунд или даже минут болыше. Вспомните, Windows не является системой реального времени. Ваш поток может возобновиться в за данный момент, но это зависит от того, какая ситуация сложится в системе к тому времени.'
Ну, здесь это, видимо, некритично.
Там, где
ОтветитьУдалить// detect and compensate for performance counter leaps
начинается метод получения миллисекунд.
Даже если все тут кроссплатформенное, виндовая часть реализована по виндовым правилам, и если проблема с SetThreadAffinityMask существует, ее надо будет исправить.
ОтветитьУдалитьПо поводу Sleep() все верно. Параметр задает только минимальное время. В UNIX дело обстоит еще интереснее -- sleep() может быть прерван сигналом, например, от сокета, который получил новые данные. В этом случае время сна может быть меньше, чем задана параметром.
> В UNIX дело обстоит еще интереснее -- sleep()
ОтветитьУдалить> может быть прерван сигналом
А блокировать сигнал от сокета нельзя?
Самое смешное, что я уже где-то года полтора читаю мануал по glibc.
Linux сдох, FreeBSD где-то установлена, но я на ней не сижу. Везде бардак. Поэтому всё как-то очень печально затормозилось.
И сейчас у меня открыта там "Обработка Сигнала", которую я так и не дочитал.
Сразу возник вопрос.
Т.е. "аппаратные" сигналы, не могут быть блокированы, как SIGKILL?
Или просто некорректно будет блокировать
сигналы в классе таймера?
Да, можно, некоторые юниксовые сигналы можно проигнорировать, например, сигнал от сокета (SIGPIPE), а некоторые нельзя, например, SIGKILL. Особо прикольно, когда своего обработчика на SIGPIPE нет, и сигнал не проигнорирован. В этом случае при использовании сокетов программа будет падать, убитая SIGPIPE'ом. Поэтому в мире юникса принято внимательно относиться к сигналам.
ОтветитьУдалитьЕсли взять просто ситуацию без контекста, то можно в таймере запретить какие-то сигналы. Но в большой программе лучше делать единую систему обработки сигналов.
QueryPerformanceFrequency глючит на многоядерных системах и компах с активным энергосбережением. Первое решается с помощью SetThreadAffinityMask, второе похоже никак. Во втором случае PerformanceFrequency не является константой и может плавать в широких пределах.
ОтветитьУдалить