- PVSM.RU - https://www.pvsm.ru -
Как, наверняка, многие знают, в WinAPI'шную функцию Sleep передаётся число миллисекунд, на сколько мы хотим уснуть. Поэтому минимум, что мы можем запросить — это уснуть на 1 миллисекунду. Но что если мы хотим спать ещё меньше? Для интересующихся, как это сделать в картинках, добро пожаловать, под кат.
Сперва напомню, что виндоус (как любая не система реального времени) не гарантирует, что поток (некоторые называют его нить, thread) будет спать именно запрошенное время. Начиная с Висты логика ОС простая. Есть некий квант времени, выделяемый потоку на выполнение (да, да, те самые 20 мс, про которые все слышали во времена 2000/XP и до сих пор слышат про это на серверных осях). И виндоус перепланирует потоки (останавливает одни потоки, запускает другие) только по истечению этого кванта. Т.е. если квант в ОС стоит в 20 мс (по умолчанию в XP было именно такое значение, например), то даже если мы запросили Sleep(1) то в худшем случае управление нам вернётся через те же самые 20 мс. Для управления этим квантом временем есть мультимедийные функции, в частности timeBeginPeriod/timeEndPeriod.
Во вторых, сделаю краткое отступление, зачем может потребоваться такая точность. Майкрософт говорит, что такая точность нужна только мультимедийным приложениям. Например, делаете вы новый WinAMP с блекджетом, и здесь очень важно, чтобы мы новый кусок аудио-данных отправляли в систему вовремя. У меня нужда была в другой области. Был у нас декомпрессор H264 потока. И был он на ffmpeg'е. И обладал он синхронным интерфейсом (Frame* decompressor.Decompress(Frame* compressedFrame)). И всё было хорошо, пока не прикрутили декомпрессию на интеловских чипах в процессорах. В силу уже не помню каких причин работать с ним пришлось не через родное интеловское Media SDK, а через DXVA2 интерфейс. А оно асинхронное. Так что пришлось работать так:
Проблема оказалась во втором пункте. Если верить GPUView, то кадры успевали расжиматься за 50-200 микросекунд. Если поставить Sleep(1) то на core i5 можно расжать максимум 1000*4*(ядра) = 4000 кадров в секунду. Если считать обычный fps равным 25, то это выходит всего 40 * 4 = 160 видеопотоков одновременно декомпрессировать. А цель стояла вытянуть 200. Собственно было 2 варианта: либо переделывать всё на асинхронную работу с аппаратным декомпрессором, либо уменьшать время Sleep'а.
Чтобы грубо оценить текущий квант времени выполнения потока, напишем простую программу:
void test()
{
std::cout << "Starting test" << std::endl;
std::int64_t total = 0;
for (unsigned i = 0; i < 5; ++i)
{
auto t1 = std::chrono::high_resolution_clock::now();
::Sleep(1);
auto t2 = std::chrono::high_resolution_clock::now();
auto elapsedMicrosec = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
total += elapsedMicrosec;
std::cout << i << ": Elapsed " << elapsedMicrosec << std::endl;
}
std::cout << "Finished. average time:" << (total / 5) << std::endl;
}
int main()
{
test();
return 0;
}
Сразу, хочу предупредить, что если у вас например MSVS 2012, то std::chrono::high_resolution_clock вы ничего не намеряете. Да и вообще, вспоминаем, что самый верный способ измерить длительность чего либо — это Performance Counter'ы. Перепишем немного наш код, чтобы быть уверенными, что меряем времена мы правильно. Для начала напишем классец-хелпер. Я тесты сейчас делал на MSVS2015, там реализация high_resolution_clock уже правильная, через performance counter'ы. Делаю этот шаг, вдруг кто захочет повторить тесты на более старом компиляторе
#pragma once
class PreciseTimer
{
public:
PreciseTimer();
std::int64_t Microsec() const;
private:
LARGE_INTEGER m_freq; // системная частота таймера.
};
inline PreciseTimer::PreciseTimer()
{
if (!QueryPerformanceFrequency(&m_freq))
m_freq.QuadPart = 0;
}
inline int64_t PreciseTimer::Microsec() const
{
LARGE_INTEGER current;
if (m_freq.QuadPart == 0 || !QueryPerformanceCounter(¤t))
return 0;
// Пересчитываем количество системных тиков в микросекунды.
return current.QuadPart * 1000'000 / m_freq.QuadPart;
}
void test()
{
PreciseTimer timer;
std::cout << "Starting test" << std::endl;
std::int64_t total = 0;
for (unsigned i = 0; i < 5; ++i)
{
auto t1 = timer.Microsec();
::Sleep(1);
auto t2 = timer.Microsec();
auto elapsedMicrosec = t2 - t1;
total += elapsedMicrosec;
std::cout << i << ": Elapsed " << elapsedMicrosec << std::endl;
}
std::cout << "Finished. average time:" << (total / 5) << std::endl;
}
Перепишем немного нашу программу. И попытаемся использовать очевидное:
void test(const std::string& description, const std::function<void(void)>& f)
{
PreciseTimer timer;
std::cout << "Starting test: " << description << std::endl;
std::int64_t total = 0;
for (unsigned i = 0; i < 5; ++i)
{
auto t1 = timer.Microsec();
f();
auto t2 = timer.Microsec();
auto elapsedMicrosec = t2 - t1;
total += elapsedMicrosec;
std::cout << i << ": Elapsed " << elapsedMicrosec << std::endl;
}
std::cout << "Finished. average time:" << (total / 5) << std::endl;
}
int main()
{
test("Sleep(1)", [] { ::Sleep(1); });
test("sleep_for(microseconds(500))", [] { std::this_thread::sleep_for(std::chrono::microseconds(500)); });
return 0;
}
Т.е. как мы видим, с ходу никакого выигрыша нету. Посмотрим внимательнее на this_thread::sleep_for. И замечаем, что он вообще реализован через this_thread::sleep_until, т.е. в отличие от Sleep он даже не иммунен к переводу часов, например. Попробуем найти лучшую альтернативу.
Поиск по MSDN и stackoverflow направляет нас в сторону Waitable Timers, как на единственную альтернативу. Что же, напишем ещё один хелперный классец.
#pragma once
class WaitableTimer
{
public:
WaitableTimer()
{
m_timer = ::CreateWaitableTimer(NULL, FALSE, NULL);
if (!m_timer)
throw std::runtime_error("Failed to create waitable time (CreateWaitableTimer), error:" + std::to_string(::GetLastError()));
}
~WaitableTimer()
{
::CloseHandle(m_timer);
m_timer = NULL;
}
void SetAndWait(unsigned relativeTime100Ns)
{
LARGE_INTEGER dueTime = { 0 };
dueTime.QuadPart = static_cast<LONGLONG>(relativeTime100Ns) * -1;
BOOL res = ::SetWaitableTimer(m_timer, &dueTime, 0, NULL, NULL, FALSE);
if (!res)
throw std::runtime_error("SetAndWait: failed set waitable time (SetWaitableTimer), error:" + std::to_string(::GetLastError()));
DWORD waitRes = ::WaitForSingleObject(m_timer, INFINITE);
if (waitRes == WAIT_FAILED)
throw std::runtime_error("SetAndWait: failed wait for waitable time (WaitForSingleObject)" + std::to_string(::GetLastError()));
}
private:
HANDLE m_timer;
};
И дополним наши тесты новым:
int main()
{
test("Sleep(1)", [] { ::Sleep(1); });
test("sleep_for(microseconds(500))", [] { std::this_thread::sleep_for(std::chrono::microseconds(500)); });
WaitableTimer timer;
test("WaitableTimer", [&timer] { timer.SetAndWait(5000); });
return 0;
}
Посмотрим, изменилось что.
Как мы видим, на сервеных операционах с ходу, ничего не поменялось. Так как по умолчанию квант времени выполнения потока на ней обычно огромный. Не буду искать виртуалки с XP и с Windows 7, но скажу, что скорее всего на XP будет полностью аналогичная ситуация, а вот на Windows 7 вроде как квант времени по умолчанию 1мс. Т.е. Новый тест должен дать те же показатели, что давали предыдущие тесты на Windows 8.1.
Что мы видим? Правильно, что наш новый слип смог! Т.е. на Windows 8.1 мы свою задачу уже решили. Из-за чего так получилось? Это произошло из-за того, что в windows 8.1 квант времени сделали как раз 500 микросекунд. Да, да, потоки выполняются по 500 микросекунд (на моей системе по умолчанию разрешение установлено в 500,8 микросекунд и меньше не выставляется, в отличие от XP/Win7 где можно было ровно в 500 микросекунд выставить), потом заново перепланируются согласно их приоритетам и запускаются на новое выполнение.
Вывод 1: Чтобы сделать Sleep(0.5) необходим, но не достаточен, правильный слип. Всегда используйте Waitable timers для этого.
Вывод 2: Если вы пишите только под Win 8.1/Win 10 и гарантированно не будете запускаться на других операционках, то на использовании Waitable Timers можно остановиться.
Я уже упоминал мультимедийную функцию timeBeginPeriod. В документации Заявляется, что с помощью этой функции можно устанавливать желаемую точностью таймера. Давайте проверим. Ещё раз модифицируем нашу программу.
#include "stdafx.h"
#include "PreciseTimer.h"
#include "WaitableTimer.h"
#pragma comment (lib, "Winmm.lib")
void test(const std::string& description, const std::function<void(void)>& f)
{
PreciseTimer timer;
std::cout << "Starting test: " << description << std::endl;
std::int64_t total = 0;
for (unsigned i = 0; i < 5; ++i)
{
auto t1 = timer.Microsec();
f();
auto t2 = timer.Microsec();
auto elapsedMicrosec = t2 - t1;
total += elapsedMicrosec;
std::cout << i << ": Elapsed " << elapsedMicrosec << std::endl;
}
std::cout << "Finished. average time:" << (total / 5) << std::endl;
}
void runTestPack()
{
test("Sleep(1)", [] { ::Sleep(1); });
test("sleep_for(microseconds(500))", [] { std::this_thread::sleep_for(std::chrono::microseconds(500)); });
WaitableTimer timer;
test("WaitableTimer", [&timer] { timer.SetAndWait(5000); });
}
int main()
{
runTestPack();
std::cout << "Timer resolution is set to 1 ms" << std::endl;
// здесь надо бы сперва timeGetDevCaps вызывать и смотреть, что она возвращяет, но так как этот вариант
// мы в итоге выкинем, на написание правильного кода заморачиваться не будем
timeBeginPeriod(1);
::Sleep(1); // чтобы предыдущие таймеры гарантированно отработали
::Sleep(1); // чтобы предыдущие таймеры гарантированно отработали
runTestPack();
timeEndPeriod(1);
return 0;
}
Традиционно, типичные выводы нашей програмы.
Давай те разберём интересные факты, которые видны из результатов:
void timeBeginPerion(UINT uPeriod)
{
if (uPeriod == 1)
{
setMaxTimerResolution();
return;
}
...
}
Замечу, что на Windows Server 2003 R2 такого ещё не было. Это нововведение в 2008м сервере.
Продолжим наши выводы:
Вывод 3: Если вы пишите только под Win Server 2008/2012/2016 и гарантированно не будете запускаться на других операционках, то можно вообще не заморачиваться, timeBeginPeriod(1) и последующие Sleep(1) будут делать всё, что вам нужно.
Вывод 4: timeBeginPeriod для наших целей хорош только под серверные оси. но совместное его использование с Waitable timer'ами, покрывает нашу задачу на Win Server 2008/2012/2016 и на Windows 8.1/Windows 10
Давай те подумаем, что же нам делать, если нам надо, чтобы Sleep(0.5) работал и под Win XP/Win Vista/Win 7/Win Server 2003.
На помощь нам придёт только native api — то недокументированное api, что нам доступно из user space через ntdll.dll. Там есть интересные функции NtQueryTimerResolution/NtSetTimerResolution.
ULONG AdjustSystemTimerResolutionTo500mcs()
{
static const ULONG resolution = 5000; // 0.5 мс в 100-наносекундных интервалах.
ULONG sysTimerOrigResolution = 10000;
ULONG minRes;
ULONG maxRes;
NTSTATUS ntRes = NtQueryTimerResolution(&maxRes, &minRes, &sysTimerOrigResolution);
if (NT_ERROR(ntRes))
{
std::cerr << "Failed query system timer resolution: " << ntRes;
}
ULONG curRes;
ntRes = NtSetTimerResolution(resolution, TRUE, &curRes);
if (NT_ERROR(ntRes))
{
std::cerr << "Failed set system timer resolution: " << ntRes;
}
else if (curRes != resolution)
{
// здесь по идее надо проверять не равенство curRes и resolution, а их отношение. Т.е. возможны случаи, например,
// что запрашиваем 5000, а выставляется в 5008
std::cerr << "Failed set system timer resolution: req=" << resolution << ", set=" << curRes;
}
return sysTimerOrigResolution;
}
#include <winnt.h>
#ifndef NT_ERROR
#define NT_ERROR(Status) ((((ULONG)(Status)) >> 30) == 3)
#endif
extern "C"
{
NTSYSAPI
NTSTATUS
NTAPI
NtSetTimerResolution(
_In_ ULONG DesiredResolution,
_In_ BOOLEAN SetResolution,
_Out_ PULONG CurrentResolution);
NTSYSAPI
NTSTATUS
NTAPI
NtQueryTimerResolution(
_Out_ PULONG MaximumResolution,
_Out_ PULONG MinimumResolution,
_Out_ PULONG CurrentResolution);
}
#pragma comment (lib, "ntdll.lib")
Осталось сделать наблюдения и выводы.
Наблюдения:
Автор: nikolaynnov
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/232589
Ссылки в тексте:
[1] Источник: https://habrahabr.ru/post/319402/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.