*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

четверг, 29 января 2009 г.

Миллисекундный таймер для Windows и UNIX

Очень часто в программе удобно иметь возможность засекать и мерить интервалы времени. Стандартная функция time() конечно хороша своей переносимостью, но она работает с секундами, а хочется что-то более быстрое. Микросекунды - это уже тоже перебор. А вот миллисекунды - самое оно.
Итак, задача: сделать простой и переносимый класс 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-тестированию, и программируйте правильно!


Другие посты по теме:

10 комментариев:

  1. Спасибо, пригодился класс для программы, тестирующей алгоритмы сортировки.
    Взял с вас привычку любить gTest

    ОтветитьУдалить
  2. Здорово! Значит главный message был доставлен по назначению.

    ОтветитьУдалить
  3. А как насчёт 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);

    ОтветитьУдалить
  4. NULL: Пока я не видел сбоя тестов на многопроцессорных и многоядерных системах. Надо будет погонять еще.

    Спасибо за замечание.

    ОтветитьУдалить
  5. Фу, переглючило слегка. У вас же он кроссплатформенный. Извиняюсь.

    Но то, что конкретно к винде относится.
    Сбоя может и не быть, но неточности.
    Как насчёт 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 не является системой реального времени. Ваш поток может возобновиться в за данный момент, но это зависит от того, какая ситуация сложится в системе к тому времени.'

    Ну, здесь это, видимо, некритично.

    ОтветитьУдалить
  6. Там, где
    // detect and compensate for performance counter leaps

    начинается метод получения миллисекунд.

    ОтветитьУдалить
  7. Даже если все тут кроссплатформенное, виндовая часть реализована по виндовым правилам, и если проблема с SetThreadAffinityMask существует, ее надо будет исправить.

    По поводу Sleep() все верно. Параметр задает только минимальное время. В UNIX дело обстоит еще интереснее -- sleep() может быть прерван сигналом, например, от сокета, который получил новые данные. В этом случае время сна может быть меньше, чем задана параметром.

    ОтветитьУдалить
  8. > В UNIX дело обстоит еще интереснее -- sleep()
    > может быть прерван сигналом
    А блокировать сигнал от сокета нельзя?
    Самое смешное, что я уже где-то года полтора читаю мануал по glibc.
    Linux сдох, FreeBSD где-то установлена, но я на ней не сижу. Везде бардак. Поэтому всё как-то очень печально затормозилось.
    И сейчас у меня открыта там "Обработка Сигнала", которую я так и не дочитал.

    Сразу возник вопрос.
    Т.е. "аппаратные" сигналы, не могут быть блокированы, как SIGKILL?
    Или просто некорректно будет блокировать
    сигналы в классе таймера?

    ОтветитьУдалить
  9. Да, можно, некоторые юниксовые сигналы можно проигнорировать, например, сигнал от сокета (SIGPIPE), а некоторые нельзя, например, SIGKILL. Особо прикольно, когда своего обработчика на SIGPIPE нет, и сигнал не проигнорирован. В этом случае при использовании сокетов программа будет падать, убитая SIGPIPE'ом. Поэтому в мире юникса принято внимательно относиться к сигналам.

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

    ОтветитьУдалить
  10. QueryPerformanceFrequency глючит на многоядерных системах и компах с активным энергосбережением. Первое решается с помощью SetThreadAffinityMask, второе похоже никак. Во втором случае PerformanceFrequency не является константой и может плавать в широких пределах.

    ОтветитьУдалить