Исследование: системный таймер Windows
От: Mr. None Россия http://mrnone.blogspot.com
Дата: 16.02.11 07:44
Оценка: 31 (4)
Навеяно спором в ветке ниже
Автор: Mr. None
Дата: 15.02.11
. Поняв бесперспективность дальнейшей дискусси, я решил разложить всё по полочкам и провести более аккуратные измерения. Листинг программы привожу по тексту ниже ибо он весьма велик.

Какие есть таймеры внутри операционной системы: ждущие таймеры, мультимедийные таймеры, таймер часов и бла-бла-бла. Всё это верхушка айсберга. В основании лежит единый системный таймер. Он же аппаратный программируемый таймер, который мы помним из школьного курса программирования аппаратных средств в MS DOS. Напомню про него в кратце: это кварц с частотой 1.19318 МГц, который управляет генерацией аппаратного прерывания IRQ0 через определённые интервалы. Частота генерации этого прерывания варьируется от 18.206 Гц (так было в MS DOS), до собственно 1.19318 МГц (предельная частота кварца). Управлять частотой генерации IRQ0 можно программно, поэтому он и называется программируемым. В цифрах мог немного ошибиться (давно это всё было) и в реалии всё чуть-чуть сложнее (на самом деле таймер имеет несколько каналов, предназначенных для разных вещей), но сути дела это не меняет. Главное что нужно понять, так это то, что частота так называемого системного таймера Windows по сути и является этой самой частотой генерации прерывания IRQ0. Опять же детали могут немного отличаться, за сим остави эту часть аппаратчикам, а сами плавно перейдём к программной части.

Частота системного таймера по-умолчанию зависит от множества факторов (например на моей Windows 7 c 3-мя ядрами она составляла 1ms), в то время как на соседнем w2k8 сервере с одним ядром примерно 15.6ms. Опять же значение по умолчанию не важно, важен тот факт, что мы можем его изменять. Либо напрямую с помощью функции режима ядра NtSetTimerResolution:
NTSTATUS NtSetTimerResolution (
  IN ULONG RequestedResolution,
  IN BOOLEAN Set,
  OUT PULONG ActualResolution
);


Либо с помощью функции timeBeginPeriod из Multimedia SDK, котороая согласно исследованиям и статье Марка Руссиновича "Inside Windows NT High Resolution Timers" действительно перенаправляет вызов функции NtSetTimerResolution и влияет на точность (частоту срабатывания) системного таймера. Казалось бы, вуаля: вот прекрасный способ увеличить точность временных функций и избавиться от задержек ожидания и так далее. Но действительность гораздо суровее, чем это кажется.

Давайте разберёмся, на что влияет изменение точности системного таймера.
Ну естественно в первую очередь он влияет на точность временных измерений. Все остальные таймеры имеют свои показатели точности и гранулярности, которые в зависимости от критичности таймера либо близки к показателю системного, либо весьма далеки от него. Так например таймер часов на моей машине имеет точность примерно те же самый 15.6ms (получить это значение можно с помощью функции GetSystemTimeAdjustment). Принцип работы остальных таймеров примерно следующий: при срабатывании IRQ0 от некоего внутреннего счётчика этого таймера отнимается показатель времени, и когда счётчик становится меньше либо равен нулю, то таймер генерит событие. Судя по всему в случае некоторых таймеров счётчик уменьшается не на каждое срабатывание IRQ0 и уменьшается не на величину точности системного таймера, а на некоторое число, вычисляемое исходя из частоты срабатывания IRQ0 и точности этого таймера. Сразу признаюсь это лишь мои предположения, которые основаны на анализе значений системного таймера и показателей точности системных часов на моей машине.

Во-вторых, и что гораздо важнее, он влияет на точность отсчёта квантов процессорного времени, выделяемого планировщиком потокам. Если верить всё той же статье Марка Руссиновича ("Windows NT High Resolution Timers"), квант отсчитывается по следующему алгоритму. При выделении потоку процессорного времени, определяется доступный ему квант в милисекундах — это значение сохраняется во внутреннюю переменную счётчик. При каждом срабатывании системного таймера, этот счётчик уменьшается на величину равную точности системного таймера. Таким образом, если квант равен 10ms, а точность системного таймера равна 1ms, то ровно через 10ms на 10-ом срабатывнии системного таймера поток будет вытеснен с процессора (если не случиться чего либо ужасного, вроде потока с Realtime приоритетом). Если же размер кванта равен тем же 10ms, а точность системного таймера 8ms, то вытеснение произойдёт через 18ms на 2-ом срабатывании таймера. В этом и только в это заключается влияение точности системного таймера на отсчёт кванта.


Теперь о том, на что не влияет точность системного таймера.
Она не влияет на размер кванта и на алгоритм работы планировщика задач. Размер кванта времени зависит от:
Общее описание алгоритмы работы планировщика тоже предельно просто и описано даже в MSDN`е:

Из этого следует, что хоть изменение точности системного таймера и должны повлиять на точность отсчёта времени в функции Sleep, в общем случае они не должны повлиять на временные задержки, связанные с пробуждением спящего потока (пока процессор не освободится поток не сможет стать планируемым). И уж точно они не могут оказать практически никакого влияния на wait-функции, разве что на случай пробуждения по тайм-ауту.

Давайте проверять.
Простейшие тесты с вызовом timeBeginPeriod(1) и замерами для нескольких вызовов функции Sleep с параметром 1 (примеров коих множество на форуме), вроде показывают несостоятельность данного предположения: Sleep(1) действительно спит около 1ms. Я осмелился предположить иное, а именно: алгоритм работы планировщика работает именно так, как описано; а вот алгоритм функции Sleep может немного отличаться, например она может не впадать в коматозное состояние, если время сна меньше разрешения системного таймера или время сна успевает истечь до того, как процесс реально уснёт, поэтому происходит его мгновенное пробуждение. Короткие замеры показывают именно это самое побочное явление функции Sleep, суть которого если честно мне не интересна. Я поставил чебе цель проверить, действительно ли вызов Sleep(1) при точности системного таймера 1ms всегда будет спать ровно 1ms и никто не будет вмешиваться в этот процесс. Отдельный интерес представляет проверка поведения wait-функций в подобных же условиях. Для этих целей я написал простое приложение, которое может быть использовано со следующими аргументами (листинг ниже):
SleepTest.exe [-realtime] [-above-normal] [-loading-thread-below] [-adjust-timer] [-min-delay <integer>] [-test-wait]
-realtime — запустить потоки с максимальным приоритетом (по умолчанию false)
-above-normal — запустить потоки с повышенным приоритетом (по умолчанию false)
-loading-thread-below — поток, эмитирующий "полезную" нагрузку запускается с приоритетом ниже, чем ждущий поток (по умолчанию false)
-adjust-timer — прменить timeBeginPeriod перед засыпанием (по умолчанию false)
-min-delay — минимальное значение задержки которое нужно выводить на экран (единица соответствует в 0.001ms; по умолчанию 150, что соответствует 1.5ms)
-test-wait — запустить тест для wait-функций вместо sleep (по умолчанию false)

Итак тестирование.
Приложение скомпилировано на MS VC 2005 для 64-ёх битной платформы в release конфигурации по-умолчанию. Тестовая платформа: Windows 7 x64 с 3-ёх ядерным процессором (к сожалению все остальные платформы, которые есть под рукой — виртуальные и данный тест не имеет на них никакого смысла, потому что повлять на точность системного таймера для них мы не сможем — она всегда определяется хостовой системой). Машина имеет следующие показания точности системного таймера (по данным приложения CloclRes.exe).
D:\SleepTest\x64\release>clockres

ClockRes v2.0 — View the system clock resolution
Copyright (C) 2009 Mark Russinovich
SysInternals — www.sysinternals.com

Maximum timer interval: 15.600 ms
Minimum timer interval: 0.500 ms
Current timer interval: 1.000 ms


Приложение функционирует следующим образом. При старте создаёт несколько потоков: генерирующий нагрузку, ждущий и (для случая тестирования wait-функций) поток генерации события. Первые 2 потока всегда создаются с привязкой к одному процессору, чтобы эмулировать конкуренцию. 3-ий поток на многоядерной системе всегда привязывается к другому процессору, чтобы не оказывать влияния на вычисления. Поток генерирующий нагрузку просто загружает процессор, ждущий поток замеряет время ожидания (время нахождения внутри вызова Sleep(1) или время между генерацией события и выходом из wait-функции) и выводит его на экран, если оно больше указанного с помощью параметра -min-delay.

1) Запуск с нормальными приоритетами, без настройки таймера, тестируем Sleep (напоминаю, точность системного таймера по-умолчанию 1ms):
D:\SleepTest\x64\release>SleepTest.exe
399: 31.99ms
441: 1.85ms
1408: 2.02ms
3200: 1.55ms
3324: 1.77ms
3354: 1.79ms
4146: 1.67ms
8678: 1.81ms
10014: 3.1ms
11304: 1.63ms
13286: 9.58ms
15475: 1.72ms
16032: 32ms
19499: 2.8ms
20046: 1.68ms

2) Запуск с нормальными приоритетами, с настройкой таймера, тестируем Sleep:
D:\SleepTest\x64\release>SleepTest.exe -adjust-timer
5117: 1.67ms
5185: 2.01ms
5195: 3.15ms
6781: 27ms
8078: 2.02ms
9762: 2.14ms
9865: 2.01ms
11884: 6.96ms
11937: 3.98ms
12939: 3.04ms
13351: 7.99ms
18347: 4.04ms

Как и следовало ожидать, результат практически не отличается от предыдущего, потому что точность системного таймера и без того равна единице. Задержки становятся всё более очевидны и заметны, при переключении приложения на задний план

3) Убедимся, что на VMware эффекта от вызова timeBeginPeriod нет (запуск с нормальными приоритетами, с настройкой таймера, тестируем Sleep на Windows Server 2008 на VMware:
C:\Temp>SleepTest.exe -adjust-timer
1: 13.24ms
2: 14.37ms
3: 15.11ms
4: 14.24ms
5: 13.92ms
6: 14.85ms
7: 14.63ms
8: 23.27ms
9: 4.74ms
10: 14.58ms
11: 14.29ms
12: 15.04ms
13: 14.75ms


4) Запуск с приоритетом real-time, с настройкой таймера, тестируем Sleep:
d:\SleepTest\x64\release>SleepTest.exe -adjust-timer -realtime
1: 31.89ms
2: 31.98ms
3: 31.99ms
4: 31.99ms
5: 31.98ms
6: 32ms
7: 31.98ms
8: 31.98ms
9: 32ms
10: 31.98ms
11: 31.98ms

Этот результат наиболее красноречиво говорит о влиянии планировщика на функции ожидания.

5) Запуск с нормальным приоритетом, с настройкой таймера, тестируем WaitForSingleObject:
d:\Projects\Tests\SleepTest\x64\release>SleepTest.exe -adjust-timer -test-wait
6090: 1.61ms
26341: 31.83ms
31954: 2.3ms
33042: 2.01ms
39156: 34.76ms
52256: 27.89ms
91951: 2.47ms
105046: 31.78ms
118118: 31.51ms
131135: 31.13ms
184144: 31.21ms
197216: 30.87ms
210403: 30.59ms
236874: 1.64ms
250030: 31.98ms
263217: 1.58ms
289289: 3.19ms
302512: 1.53ms
342117: 3.15ms
355197: 40.36ms

Результат говорит сам за себя...

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

Текст тестового приложения.
#include "stdafx.h"

#include <Windows.h>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <process.h>
#include <string>
#include <vector>

static const int MAX_BUFFER_SIZE = 10000;
std::vector<int> buffer;

struct SleepingData {
    bool adjustTimer;
    long minDelay;
};

struct WaitingData {
    bool adjustTimer;
    long minDelay;
    HANDLE waitEvent;
    HANDLE readyForWaitingEvent;
    LARGE_INTEGER setEventTime;
};

void
work()
{
    for (int i = MAX_BUFFER_SIZE - 1; i > 0; --i) {
        buffer.push_back(i);
    }

    std::sort(buffer.begin(), buffer.end());
}

unsigned
usefullWork(void *data)
{
    while (true) {
        work();
    }

    return 0;
}

unsigned
sleeping(void *data)
{
    SleepingData *sleepingData = reinterpret_cast<SleepingData*>(data);

    LARGE_INTEGER frequency;
    ::QueryPerformanceFrequency(&frequency);

    __int64 step = 0;
    while (true) {
        LARGE_INTEGER time1;
        LARGE_INTEGER time2;

        if (sleepingData->adjustTimer) {
            timeBeginPeriod(1);
        }

        ::QueryPerformanceCounter(&time1);
        Sleep(1);
        QueryPerformanceCounter(&time2);

        if (sleepingData->adjustTimer) {
            timeEndPeriod(1);
        }
        ++step;
        __int64 delay = ((time2.QuadPart - time1.QuadPart) * 100000) / frequency.QuadPart;

        if (delay > sleepingData->minDelay) {
            std::wcout << step << L": "<< delay / 100.0 << L"ms" << std::endl;
        }
    }

    return 0;
}

unsigned
eventGenerator(void *data)
{
    WaitingData *waitingData = reinterpret_cast<WaitingData*>(data);

    while (true) {
        ::WaitForSingleObject(waitingData->readyForWaitingEvent, INFINITE);
        QueryPerformanceCounter(&waitingData->setEventTime);
        ::SetEvent(waitingData->waitEvent);
    }

    return 0;
}

unsigned
waiting(void *data)
{
    LARGE_INTEGER frequency;
    ::QueryPerformanceFrequency(&frequency);

    __int64 step = 0;
    WaitingData *waitingData = reinterpret_cast<WaitingData*>(data);
    while (true) {
        if (waitingData->adjustTimer) {
            timeBeginPeriod(1);
        }

        LARGE_INTEGER completeWaitTime;

        ::SetEvent(waitingData->readyForWaitingEvent);
        ::WaitForSingleObject(waitingData->waitEvent, INFINITE);
        QueryPerformanceCounter(&completeWaitTime);

        if (waitingData->adjustTimer) {
            timeEndPeriod(1);
        }
        ++step;
        __int64 delay = ((completeWaitTime.QuadPart - waitingData->setEventTime.QuadPart) * 100000) / frequency.QuadPart;

        if (delay > waitingData->minDelay) {
            std::wcout << step << L": "<< delay / 100.0 << L"ms" << std::endl;
        }
    }

    return 0;
}

void usage()
{
    std::wcout << L"SleepTest.exe [-realtime] [-above-normal] [-loading-thread-below] [-adjust-timer] [-min-delay <integer>] [-test-wait]" << std::endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
    bool procRealTime = false;
    bool testWait = false;
    long sleepingThreadPriority = THREAD_PRIORITY_NORMAL;
    long usefullWorkThreadPriority = THREAD_PRIORITY_NORMAL;

    buffer.reserve(MAX_BUFFER_SIZE);

    SleepingData sleepingData;
    sleepingData.adjustTimer = false;
    sleepingData.minDelay = 150;

    for (int idx = 1; idx < argc; ++idx) {
        std::wstring argument = argv[idx];
        if (L"-realtime" == argument) {
            procRealTime = true;
            sleepingThreadPriority = THREAD_PRIORITY_HIGHEST;
            usefullWorkThreadPriority = THREAD_PRIORITY_HIGHEST;
        } else if (L"-above-normal" == argument) {
            sleepingThreadPriority = THREAD_PRIORITY_HIGHEST;
            usefullWorkThreadPriority = THREAD_PRIORITY_HIGHEST;
        } else if (L"-loading-thread-below" == argument) {
            usefullWorkThreadPriority = THREAD_PRIORITY_LOWEST;
        } else if (L"-adjust-timer" == argument) {
            sleepingData.adjustTimer = true;
        } else if (L"-min-delay" == argument) {
            ++idx;
            if (idx < argc) {
                sleepingData.minDelay = _wtol(argv[idx]);
            } else {
                usage();
                return -1;
            }
        } else if (L"-test-wait" == argument) {
            testWait = true;
        } else {
            usage();
            return -1;
        }
    }

    WaitingData waitingData;
    waitingData.waitEvent = ::CreateEvent(0, FALSE, FALSE, 0);
    waitingData.adjustTimer = sleepingData.adjustTimer;
    waitingData.minDelay = sleepingData.minDelay;
    waitingData.readyForWaitingEvent = ::CreateEvent(0, FALSE, FALSE, 0);
    waitingData.setEventTime.QuadPart = 0;

    HANDLE usefullWorkThread = 0;
    HANDLE sleepingThread = 0;
    HANDLE eventGeneratorThread = 0;

    if (testWait) {
        usefullWorkThread = reinterpret_cast<HANDLE>(
            _beginthreadex(0, 0, &usefullWork, 0, CREATE_SUSPENDED, 0));

        sleepingThread = reinterpret_cast<HANDLE>(
            _beginthreadex(0, 0, &waiting, reinterpret_cast<void*>(&waitingData), CREATE_SUSPENDED, 0));

        eventGeneratorThread = reinterpret_cast<HANDLE>(
            _beginthreadex(0, 0, &eventGenerator, reinterpret_cast<void*>(&waitingData), CREATE_SUSPENDED, 0));
        ::SetThreadAffinityMask(usefullWorkThread, 0x02);
        if (THREAD_PRIORITY_NORMAL != sleepingThreadPriority) {
            ::SetThreadPriority(eventGeneratorThread, sleepingThreadPriority);
        }
        ::ResumeThread(eventGeneratorThread);

    } else {
        usefullWorkThread = reinterpret_cast<HANDLE>(
            _beginthreadex(0, 0, &usefullWork, 0, CREATE_SUSPENDED, 0));

        sleepingThread = reinterpret_cast<HANDLE>(
            _beginthreadex(0, 0, &sleeping, reinterpret_cast<void*>(&sleepingData), CREATE_SUSPENDED, 0));
    }

    ::SetThreadAffinityMask(usefullWorkThread, 0x01);
    ::SetThreadAffinityMask(sleepingThread, 0x01);

    if (procRealTime) {
        ::SetPriorityClass(::GetCurrentProcess(), REALTIME_PRIORITY_CLASS);
    }

    if (THREAD_PRIORITY_NORMAL != usefullWorkThreadPriority) {
        ::SetThreadPriority(usefullWorkThread, usefullWorkThreadPriority);
    }
    if (THREAD_PRIORITY_NORMAL != sleepingThreadPriority) {
        ::SetThreadPriority(sleepingThread, sleepingThreadPriority);
    }

    ::ResumeThread(usefullWorkThread);
    ::ResumeThread(sleepingThread);

    wchar_t ch = std::wcin.get();

    ::TerminateThread(usefullWorkThread, 0);
    ::TerminateThread(sleepingThread, 0);
    ::CloseHandle(usefullWorkThread);
    ::CloseHandle(sleepingThread);
    ::CloseHandle(waitingData.waitEvent);
    return 0;
}
Компьютер сделает всё, что вы ему скажете, но это может сильно отличаться от того, что вы имели в виду.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.