Из­ме­ре­ние ин­тер­ва­лов вре­ме­ни в 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 — чи­та­ет из про­цес­сор­но­го счёт­чи­ка чис­ло так­тов, про­шед­шее с мо­мен­та за­пус­ка про­цес­со­ра. Са­мый точ­ный счёт­чик из до­ступ­ных. Од­на­ко име­ют­ся сле­дую­щие про­бле­мы:
    • Ча­сто­та про­цес­со­ра не­из­ве­ст­на. Бо­лее то­го, она мо­жет ме­нять­ся со вре­ме­нем в за­ви­си­мо­сти от ре­жи­ма энер­го­сбе­ре­же­ния.
    • В мно­го­ядер­ных про­цес­со­рах и мно­го­про­цес­сор­ных си­сте­мах счёт­чик свой у каж­до­го ядра́/про­цес­со­ра. И эти счёт­чи­ки не син­хро­ни­зи­ро­ва­ны, так как я́дра/про­цес­со­ры мо­гут быть за­пу­ще­ны в раз­ные мо­мен­ты вре­ме­ни, и да­же мо­гут быть на не­ко­то­рое вре­мя при­оста­нов­ле­ны. По­это­му по­ка­за­ние это­го счёт­чи­ка бу­дет «пры­гать» при пе­ре­хо­де по­то­ка с од­но­го яд­ра на дру­гое.
  • QueryPerformance­Counter() — «тай­мер вы­со­ко­го раз­ре­ше­ния». Вве­дён фир­мой Microsoft, что­бы раз и на­все­гда по­ста­вить точ­ку в про­бле­мах из­ме­ре­ния вре­ме­ни. Ча­сто­та это­го тай­ме­ра (1 МГц и вы­ше) не ме­ня­ет­ся во вре­мя ра­бо­ты си­сте­мы. Ча­сто­ту мож­но узнать с по­мо­щью функ­ции Query­Per­for­mance­Fre­quen­cy(). Для каж­дой си­сте­мы 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

Шесть отзывов на запись «Из­ме­ре­ние ин­тер­ва­лов вре­ме­ни в Windows»

Спа­си­бо за ста­тью.
един­ствен­ное я её дол­го ис­кал, на­шел толь­ко че­рез QueryPerformanceCounter. до­бавь для неё те­ги , что то ти­па «из­ме­ре­ние ско­ро­сти ко­да».

а у ме­ня ка­сяк пол­ный:

// var timer_frequency: Int64;
QueryPerformanceFrequency(timer_frequency);
ShowMessage(IntToStr(timer_frequency));

по­ка­зы­ва­ет все­гда 2833030000…и как это в ча­сто­ту пе­ре­счи­тать?
а вре­мя вы­пол­не­ния ко­да что я счи­таю по­лу­ча­ет­ся де­вять де­ся­ти­ты­сяч­ных…%(

2833030000 — это и есть ча­сто­та. 3 ги­га­гер­ца при­мер­но.

уж не вы ли это?о_О
вы с Ма­сте­ров/DeskSoft?х_Х

зна­чит де­вять де­ся­ти­ты­сяч­ных — нор­маль­ный ре­зуль­тат?

Нет, это не я.

Очень по­лез­ная ста­тья. Спа­си­бо.

Прав­да, хо­те­лось бы ре­а­би­ли­ти­ро­вать функ­цию GetSystemTime(). Точ­ность ее до­воль­но вы­со­ка (опре­де­ля­ет­ся ча­са­ми RTC на ма­те­рин­ке). Син­хро­ни­за­ция обыч­но раз в две не­де­ли. От лет­не­го вре­ме­ни не за­ви­сит, вы­ра­жа­ет­ся в UTC (по Грин­ви­чу). Лет­нее вре­мя вли­я­ет на GetLocalTime(). Да­ет от­счет в 100-на­но­се­кунд­ных ин­тер­ва­лах, на­чи­ная с 1 ян­ва­ря 1601 го­да. Кста­ти, это был по­не­дель­ник. Ес­ли не нра­вит­ся ст­рук­ту­ра SYSTEMTIME, пе­ре­во­дим в FILETIME функ­ци­ей SystemTimeToFileTime(). А это тот же LARGE_INTEGER, ко­то­рым вы поль­зу­е­тесь. Лич­но мне нра­вит­ся.

Оставить отзыв

Жёлтые поля обязательны к заполнению

   

Можете использовать теги <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>