Как, наверняка, многие знают, в 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 интерфейс. А оно асинхронное. Так что пришлось работать так:

  • Копируем данные в видеопамять
  • Делаем Sleep, чтобы кадр успел расжаться
  • Опрашиваем, завершилась ли декомпрессия, и если да, то забираем расжатый кадр из видеопамяти

Проблема оказалась во втором пункте. Если верить 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;
}

Вот типичный вывод на Win 8.1
Starting test
0: Elapsed 1977
1: Elapsed 1377
2: Elapsed 1409
3: Elapsed 1396
4: Elapsed 1432
Finished. average time:1518

Сразу, хочу предупредить, что если у вас например MSVS 2012, то std::chrono::high_resolution_clock вы ничего не намеряете. Да и вообще, вспоминаем, что самый верный способ измерить длительность чего либо — это Performance Counter'ы. Перепишем немного наш код, чтобы быть уверенными, что меряем времена мы правильно. Для начала напишем классец-хелпер. Я тесты сейчас делал на MSVS2015, там реализация high_resolution_clock уже правильная, через performance counter'ы. Делаю этот шаг, вдруг кто захочет повторить тесты на более старом компиляторе

PreciseTimer.h
#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;
}


Изменённая функция test
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;
}

Ну и типичный вывод нашей программы на Windows Server 2008 R2
Starting test
0: Elapsed 10578
1: Elapsed 14519
2: Elapsed 14592
3: Elapsed 14625
4: Elapsed 14354
Finished. average time:13733

Пытаемся решить проблему в лоб


Перепишем немного нашу программу. И попытаемся использовать очевидное:

std::this_thread::sleep_for(std::chrono::microseconds(500))
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;
}

Типичный вывод на Windows 8.1
Starting test: Sleep(1)
0: Elapsed 1187
1: Elapsed 1315
2: Elapsed 1427
3: Elapsed 1432
4: Elapsed 1449
Finished. average time:1362
Starting test: sleep_for(microseconds(500))
0: Elapsed 1297
1: Elapsed 1434
2: Elapsed 1280
3: Elapsed 1451
4: Elapsed 1459
Finished. average time:1384

Т.е. как мы видим, с ходу никакого выигрыша нету. Посмотрим внимательнее на this_thread::sleep_for. И замечаем, что он вообще реализован через this_thread::sleep_until, т.е. в отличие от Sleep он даже не иммунен к переводу часов, например. Попробуем найти лучшую альтернативу.

Слип, который может


Поиск по MSDN и stackoverflow направляет нас в сторону Waitable Timers, как на единственную альтернативу. Что же, напишем ещё один хелперный классец.

WaitableTimer.h
#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;
}

Посмотрим, изменилось что.

Типичный вывод на Windows Server 2008 R2
Starting test: Sleep(1)
0: Elapsed 10413
1: Elapsed 8467
2: Elapsed 14365
3: Elapsed 14563
4: Elapsed 14389
Finished. average time:12439
Starting test: sleep_for(microseconds(500))
0: Elapsed 11771
1: Elapsed 14247
2: Elapsed 14323
3: Elapsed 14426
4: Elapsed 14757
Finished. average time:13904
Starting test: WaitableTimer
0: Elapsed 12654
1: Elapsed 14700
2: Elapsed 14259
3: Elapsed 14505
4: Elapsed 14493
Finished. average time:14122

Как мы видим, на сервеных операционах с ходу, ничего не поменялось. Так как по умолчанию квант времени выполнения потока на ней обычно огромный. Не буду искать виртуалки с XP и с Windows 7, но скажу, что скорее всего на XP будет полностью аналогичная ситуация, а вот на Windows 7 вроде как квант времени по умолчанию 1мс. Т.е. Новый тест должен дать те же показатели, что давали предыдущие тесты на Windows 8.1.

А теперь поглядим на вывод нашей программы на Windows 8.1
Starting test: Sleep(1)
0: Elapsed 1699
1: Elapsed 1444
2: Elapsed 1493
3: Elapsed 1482
4: Elapsed 1403
Finished. average time:1504
Starting test: sleep_for(microseconds(500))
0: Elapsed 1259
1: Elapsed 1088
2: Elapsed 1497
3: Elapsed 1497
4: Elapsed 1528
Finished. average time:1373
Starting test: WaitableTimer
0: Elapsed 643
1: Elapsed 481
2: Elapsed 424
3: Elapsed 330
4: Elapsed 468
Finished. average time:469

Что мы видим? Правильно, что наш новый слип смог! Т.е. на 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. В документации Заявляется, что с помощью этой функции можно устанавливать желаемую точностью таймера. Давайте проверим. Ещё раз модифицируем нашу программу.

программа v3
#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;
}


Традиционно, типичные выводы нашей програмы.

На Windows 8.1
Starting test: Sleep(1)
0: Elapsed 2006
1: Elapsed 1398
2: Elapsed 1390
3: Elapsed 1424
4: Elapsed 1424
Finished. average time:1528
Starting test: sleep_for(microseconds(500))
0: Elapsed 1348
1: Elapsed 1418
2: Elapsed 1459
3: Elapsed 1475
4: Elapsed 1503
Finished. average time:1440
Starting test: WaitableTimer
0: Elapsed 200
1: Elapsed 469
2: Elapsed 442
3: Elapsed 456
4: Elapsed 462
Finished. average time:405
Timer resolution is set to 1 ms
Starting test: Sleep(1)
0: Elapsed 1705
1: Elapsed 1412
2: Elapsed 1411
3: Elapsed 1441
4: Elapsed 1408
Finished. average time:1475
Starting test: sleep_for(microseconds(500))
0: Elapsed 1916
1: Elapsed 1451
2: Elapsed 1415
3: Elapsed 1429
4: Elapsed 1223
Finished. average time:1486
Starting test: WaitableTimer
0: Elapsed 602
1: Elapsed 445
2: Elapsed 994
3: Elapsed 347
4: Elapsed 345
Finished. average time:546

И на Windows Server 2008 R2
Starting test: Sleep(1)
0: Elapsed 10306
1: Elapsed 13799
2: Elapsed 13867
3: Elapsed 13877
4: Elapsed 13869
Finished. average time:13143
Starting test: sleep_for(microseconds(500))
0: Elapsed 10847
1: Elapsed 13986
2: Elapsed 14000
3: Elapsed 13898
4: Elapsed 13834
Finished. average time:13313
Starting test: WaitableTimer
0: Elapsed 11454
1: Elapsed 13821
2: Elapsed 14014
3: Elapsed 13852
4: Elapsed 13837
Finished. average time:13395
Timer resolution is set to 1 ms
Starting test: Sleep(1)
0: Elapsed 940
1: Elapsed 218
2: Elapsed 276
3: Elapsed 352
4: Elapsed 384
Finished. average time:434
Starting test: sleep_for(microseconds(500))
0: Elapsed 797
1: Elapsed 386
2: Elapsed 371
3: Elapsed 389
4: Elapsed 371
Finished. average time:462
Starting test: WaitableTimer
0: Elapsed 323
1: Elapsed 338
2: Elapsed 309
3: Elapsed 359
4: Elapsed 391
Finished. average time:344

Давай те разберём интересные факты, которые видны из результатов:

  1. На windows 8.1 ничего не поменялось. Делаем вывод, что timeBeginPeriod достаточно умный, т.е. если N приложений запросили разрешение системного таймера в разные значения, то понижаться это разрешение не будет. На Windows 7 мы бы тоже не заметили никаких изменений, так как там разрешение таймера уже стоит в 1 мс.

  2. На серверной операционке, timeBeginPeriod(1) отработал неожиданным образом: он установил разрешение системного таймера в наибольшее возможное значение. Т.е. на таких операционках где-то явно зашит воркараунт вида:

    void timeBeginPerion(UINT uPeriod)
    {
    	if (uPeriod == 1)
    	{
    		setMaxTimerResolution();
    		return;
    	}
    	...
    }

    Замечу, что на Windows Server 2003 R2 такого ещё не было. Это нововведение в 2008м сервере.

  3. На серверной операционке, Sleep(1) отработал также неожиданным образом. Т.е. Sleep(1) трактуется на серверных операционках, начиная с 2008го сервера не как "сделай паузу в 1 миллисекунду", а как "сделай минимально возможную паузу". Дальше будет случай, что это утверждение не верно.

Продолжим наши выводы:

Вывод 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.

Напишем функцию AdjustSystemTimerResolutionTo500mcs.
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")


Типичный вывод с Windows 8.1
Starting test: Sleep(1)
0: Elapsed 13916
1: Elapsed 14995
2: Elapsed 3041
3: Elapsed 2247
4: Elapsed 15141
Finished. average time:9868
Starting test: sleep_for(microseconds(500))
0: Elapsed 12359
1: Elapsed 14607
2: Elapsed 15019
3: Elapsed 14957
4: Elapsed 14888
Finished. average time:14366
Starting test: WaitableTimer
0: Elapsed 12783
1: Elapsed 14848
2: Elapsed 14647
3: Elapsed 14550
4: Elapsed 14888
Finished. average time:14343
Timer resolution is set to 1 ms
Starting test: Sleep(1)
0: Elapsed 1175
1: Elapsed 1501
2: Elapsed 1473
3: Elapsed 1147
4: Elapsed 1462
Finished. average time:1351
Starting test: sleep_for(microseconds(500))
0: Elapsed 1030
1: Elapsed 1376
2: Elapsed 1452
3: Elapsed 1335
4: Elapsed 1467
Finished. average time:1332
Starting test: WaitableTimer
0: Elapsed 105
1: Elapsed 394
2: Elapsed 429
3: Elapsed 927
4: Elapsed 505
Finished. average time:472

Типичный вывод с Windows Server 2008 R2
Starting test: Sleep(1)
0: Elapsed 7364
1: Elapsed 14056
2: Elapsed 14188
3: Elapsed 13910
4: Elapsed 14178
Finished. average time:12739
Starting test: sleep_for(microseconds(500))
0: Elapsed 11404
1: Elapsed 13745
2: Elapsed 13975
3: Elapsed 14006
4: Elapsed 14037
Finished. average time:13433
Starting test: WaitableTimer
0: Elapsed 11697
1: Elapsed 14174
2: Elapsed 13808
3: Elapsed 14010
4: Elapsed 14054
Finished. average time:13548
Timer resolution is set to 1 ms
Starting test: Sleep(1)
0: Elapsed 10690
1: Elapsed 14308
2: Elapsed 768
3: Elapsed 823
4: Elapsed 803
Finished. average time:5478
Starting test: sleep_for(microseconds(500))
0: Elapsed 983
1: Elapsed 955
2: Elapsed 946
3: Elapsed 937
4: Elapsed 946
Finished. average time:953
Starting test: WaitableTimer
0: Elapsed 259
1: Elapsed 456
2: Elapsed 453
3: Elapsed 456
4: Elapsed 460
Finished. average time:416

Осталось сделать наблюдения и выводы.

Наблюдения:

  1. На Win8 после первого запуска программы разрешение системного таймера сбросилось в большое значение. Т.е. вывод 2 был нами сделан неправильно.

  2. После ручной установки разброс реальных слипов для случая WaitableTimer вырос, хоть в среднем слип и держится около 500 микросекунд.

  3. На серверной операционке очень неожиданно перестал работать Sleep(1) (как и this_thread::sleep_for) по сравнению со случаем timeBeginPeriod. Т.е. Sleep(1) стал работать как он должен, в значении "сделай паузу в 1 миллисекунду".

Финальные выводы


  • Вывод 1 остался без изменения: Чтобы сделать Sleep(0.5) необходим, но не достаточен, правильный слип. Всегда используйте Waitable timers для этого.

  • Вывод 2: Разрешение системного таймера на винде зависит от типа виндоус, от версии виндоус, от запущенных в текущий момент процессов, от того, какие процессы могли выполнять до этого. Т.е. что-либо утверждать или гарантировать нельзя! Если нужны какие гарантии, то надо самому всегда запрашивать/выставлять нужную точность. Для значений меньше 1 миллисекунды нужно использовать native api. Для больших значений лучше использовать timeBeginPeriod.

  • Вывод 3: По возможности лучше тестировать код не только на своей рабочей Win 10, но и на той, что указана основной у заказчика. Надо помнить, что серверные операционки могут сильно отличаться от десктопных
Поделиться с друзьями
-->

Комментарии (63)


  1. HOMPAIN
    12.01.2017 19:07
    +1

    А так нельзя сделать?

    long t = timer.Microsec();
    while((timer.Microsec()-t)<500){}
    

    Всё равно же за это время операционка потоки не успеет перераспределить и реально «ждать» необязательно


    1. DistortNeo
      12.01.2017 19:44
      +4

      Как это не успеет? Напишите простое пинг-понг приложение и посчитайте, сколько раз будет передано управление из одного потока в другой. Увидите от 10 до 100 тысяч переключений в секунду.


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


    1. iCpu
      13.01.2017 07:49
      +4

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


    1. nikolaynnov
      13.01.2017 11:23
      +1

      Это загрузило бы процессор конкретно, а нам ведь надо с этими расжатыми кадрами ещё много что сделать. Подход на таймерах/слипах позволяет процессору это время выполнять другие потоки.


  1. Tibor128
    12.01.2017 19:11
    +1

    Интересно, а как select будет себя вести? Не пробовали сравнить?


  1. hacenator
    12.01.2017 20:31

    «разжимали» бы несколько кадров пока ждете миллисекунду.


    1. nikolaynnov
      13.01.2017 11:21

      Проблема в том, что как я сказал у нас синхронный интерфейс декомпрессора. Т.е. сверху нам никто не даст следующий кард, пока мы не разожмём текущий. Собственно, как я уже говорил, у нас был выбор, либо перерабатываем архитектуру, чтобы самим расжимать асинхронно, либо пытаемся сделать меньший слип. Выбрали второе.


      1. mayorovp
        13.01.2017 12:36

        А нельзя параллельно разжимать кадры? Один поток разжимает один кадр, второй поток разжимает другой кадр… Вы же говорили про 200 видеопотоков.


        1. nikolaynnov
          13.01.2017 13:43

          Так так и делаем. Только ядра-то всего 4.
          Смотрите, очевидно, что со слипом в 1 миллисекунду, за 1 секунду можно разжать 1000 кадров. Это на одном ядре. При среднем fps равным 25, это всего 40 потоков. Т.е. на 4-х ядерном проце получается всего 160 потоков (4000 кадров в секунду). А цель: 200 потоков, т.е. 200 * 25 = 5000 кадров в секунду.


          1. mayorovp
            13.01.2017 14:02

            А вы не пробовали просто запустить параллельно 200 потоков, а не 4?


            Да, вы сами не можете проснуться когда преобразование кадра закончилось. Но драйвер-то наверняка это знает! А значит, пока один поток спит свои 16 милисекунд, другие потоки на том же самом ядре смогут делать свою работу.


            1. nikolaynnov
              13.01.2017 14:05

              Пробовали, но большего чип от интела не позволяет. Только 4 потока, иначе они они уже будут ниже синхронизироваться за доступ к аппаратным ресурсам.


              1. mayorovp
                13.01.2017 14:07

                Ну и пусть синхронизируются. Важно лишь, что там ниже у них не будет нижнего ограничения на время сна.


                1. nikolaynnov
                  13.01.2017 14:19

                  Там возникают другие проблемы. Вплоть до того, что на накладных расходах много теряется. да и просто 200 мегов только на стеки — это уже много. Да и отлаживаться потом с таким кол-вом потоков сложновато будет. Тут пул-потоков — самое очевидное решение.

                  Да и вообще 25 fps — это 1 кадр в 40 миллисекунд. Т.е. нам надо, чтобы раз в 40 миллисекунд винда нам выделяла хоть немного времени, чтобы мы успели выгребсти результат предыдущего декодирования, вернули его наверх, нам чтобы спустили новую порцию данных, которые мы бы также запихнули в декомпрессор. Предположим, что винда переключает потоки раз в 15 миллисекунд, т.е. за это время на 4-х ядерном проце успее поработать всего 12 потоков. Пусть мы реально быстро делаем подобные операции (выгребсти ...., запихнуть), скажем 1 миллисекунду, и тут же вызываем yield, чтобы остаток кванта отдать другому потоку. (Ну либо точность таймера 1 миллисекунда). В таком случае, успеет поработать 40*4 = 160 потоков. Хм. что-то всё равно не сходится, надо подумать. Вроде как вариант с 200-та потоками работал (но плохо).


                  1. nikolaynnov
                    13.01.2017 14:27

                    Вообще, если память мне не изменяет, то на 200-х потоках, мы так и не достигли своей цели, расжималось что-то около 180 потоков (ну т.е. 4500 кадров в секунду, при нужных 5000).


                  1. mayorovp
                    13.01.2017 14:37

                    Вы переоцениваете сложность копирования. Если у вас 4 потока успевали все копировать за отведенное время — то и 200 потоков ту же самую задачу должны успеть выполнить. Суммарный объем-то не поменялся!


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


                    Проблему при таком подходе я ожидаю в стабильности. Если в четырех-поточном варианте при мгновенном перегрузе один поток задержится на лишнюю половину миллисекунды — то в варианте с 200 потоками куча потоков задержатся на лишние 16 миллисекунд.


                    Кроме того, многое зависит от реализации на стороне драйвера. Там внутри тоже может слип на 16 миллисекунд стоять :)


                    Потому и было интересно пробовали ли и что получилось.


                    1. nikolaynnov
                      13.01.2017 14:49

                      В теории да, должно работать.

                      В пробовали, но что-то не дотягивали. В итоге вернулись к пулу потоков ик слипу в 0.5 миллисекунды.


  1. tsklab
    12.01.2017 21:16
    -4

    Но что если мы хотим спать ещё меньше?
    HPET


    1. DistortNeo
      12.01.2017 22:08
      +2

      Поясните. С помощью QueryPerformanceCounter и QueryPerformanceFrequency можно точно замерять интервалы — это уже давно всем известно. А вот заставить операционную систему напрямую использовать HPET для вызова кода по таймеру все равно не получится, здесь придётся писать свою операционную систему.


      1. tsklab
        12.01.2017 22:45
        -1

        1. DistortNeo
          12.01.2017 22:50
          +1

          Ну да, Stopwatch — обёртка над функциями QueryPerformanceCounter и QueryPerformanceFrequency из API.
          Но как это поможет нам сделать Sleep на полсекунды, мне непонятно.


          Разве что busy wait с периодической проверкой таймера. Можно вычисление биткоинов запихать, чтоб процессор совсем вхолостую не работал.


          1. Ambroyz
            13.01.2017 12:11

            речь была не про «полсекунды» и не про «ровно полсекунды», а как спать меньше миллисекунды.


            1. DistortNeo
              13.01.2017 13:03
              +1

              Почему же? В посте речь идёт именно о полсекунды. Почему полсекунды — да потому что это минимальной возможный интервал системного таймера в Windows. А загвоздка в том, что sleep принимает целое число в миллисекундах, и поэтому приходится использовать платформозависимый API,


              1. nikolaynnov
                13.01.2017 13:46

                Только не «полсекунды», а «полмиллисекунды».


  1. tsklab
    12.01.2017 22:40
    -3

    1. nikolaynnov
      13.01.2017 11:29

      Если вы внимательно прочитали статью, что для замеров временных интервалов, я так же использую QueryPerformanceCounter'ы.


  1. nckma
    12.01.2017 22:53
    +1

    Честно говоря не думаю, что использование (любых) слипов — это вообще хорошее решение.
    Ожидать событие в цикле со слипом можно только в некритических приложениях. Для целей обработки видео и аудио — это как-то очень не аккуратно.
    Вы же сами пишите "Т.е. что-либо утверждать или гарантировать нельзя!". Значит нужно искать решения, где алгоритм будет гарантировать передачу блоков данных точно в нужное время. Наверняка АПИ предполагает какие-то колбэки или события которые можно ждать не в слипах, а скажем в waitforsingleobject(..) или подобных функциях.


    1. DistortNeo
      12.01.2017 22:58

      Автор так и написал:


      Собственно было 2 варианта: либо переделывать всё на асинхронную работу с аппаратным декомпрессором, либо уменьшать время Sleep'а.

      Видимо, костыль в виде второго варианта оказался проще.


      1. nckma
        12.01.2017 23:11
        -1

        То есть статья про то, как сделать кривой костыль?


        1. DistortNeo
          12.01.2017 23:14
          +2

          Нет, статья о том, как сделать Sleep на полсекунды в Windows. А то, что применяется как костыль — на то воля программиста.


          У меня не было необходимости делать такой точный Sleep, хотя и писал свой планировщик задач для асинхронного выполнения, но, тем не менее, мне это было интересно.


          1. nckma
            13.01.2017 08:51

            Это действительно кажется интересным. Однако, проблема состоит в том, что везде, где якобы требуется такой точный слип его применение окажется костылем и странным архитектурным решением.


            1. mayorovp
              13.01.2017 08:54
              +2

              В данном случае "странное архитектурное решение" находится внутри DXVA2 и сделать с этим ничего нельзя.


  1. zoonman
    12.01.2017 23:10
    +1

    Видел в исходниках PHP такое.


    1. nikolaynnov
      13.01.2017 11:34

      Интересно. Waitable timer'ы используются, это гуд, но кода поднимающего разрешение таймера не видно. Т.е. как показывают тесты, на серверных операционках это спокойно может больше 10 миллисекунд отрабатывать.


  1. lexasss
    13.01.2017 00:25

    Может, SwitchToThread() могла бы здесь справиться лучше таймера?

    for (;;) {
      if (frame_is_ready())
        break;
      SwitchToThread();
    }
    

    Честно говоря, сам такого никогда не пробовал, не обессудьте если совет в молоко.


    1. mayorovp
      13.01.2017 08:43
      +2

      Нет гарантии, что другой поток достаточно быстро вернет управление обратно. Поток же, который ожидает таймера, при пробуждении получает повышенный приоритет (на клиентских осях).


      Также потоку можно явно поставить повышенный или высокий приоритет. В таком случае таймер будет будить его сразу же при срабатывании, а вот SwitchToThread() просто перестанет работать как задумывалось.


      1. nikolaynnov
        13.01.2017 11:39

        при пробуждении получает повышенный приоритет (на клиентских осях).

        Ой, я был уверен, что на серверных всё так же с динамическим повышением приоритетов, как и в клиентских осях :-(. Надо будет себе на заметку взять провести тесты на эту тему.


        1. mayorovp
          13.01.2017 12:38

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


    1. nikolaynnov
      13.01.2017 11:37

      SwithToThread просто отдаст остаток кванта времени другому потоку. Но это не поменяет время когда система начнёт планировать потоки на следующий квант. Т.е. это мало будет отличимо от Sleep(1).


  1. ARad
    13.01.2017 06:19

    Использовать Sleep(0) не пробовали? Там выполнение передается потоку по приоритету и может обратно вернуться без ожидания если более приоритетных потоков нет.


    1. mayorovp
      13.01.2017 08:49
      +1

      Та же проблема что и с SwitchToThread() (см. выше) — нет гарантии что управление вернется обратно быстро, равно как и нет гарантии что оно не вернется сразу же.


      1. ARad
        13.01.2017 20:56

        Выполнение перейдет в другой поток декодирования, и как только у них не станет работы вернется в ваш, и не станет ждать 20 мс. Т.е. оно начнет загружать потоки по полной и нормально распределять приоритеты, так как потоки не будут находиться в очереди таймера, а только в нормальной очереди. И только если система будет перегружена работой, тогда управление будет возвращаться реже, но это и при ожидании таймера будет происходить. Т.е. 0 задержка это передача выполнения наиболее приоритетному потоку с точки зрения ОС.


  1. hdfan2
    13.01.2017 07:46
    +1

    Есть некий квант времени, выделяемый потоку на выполнение (да, да, те самые 20 мс)

    Насколько я понял (сам в своё время этим же занимался), там не 20мс, а 1/64 с. (т.е. 15-16 мс.)


    1. nikolaynnov
      13.01.2017 11:41

      Да, в реальности 15-16 миллисекунд всегда было. Это и в тестах на Win Server'е видно. Но в разговорной среде почему-то говорилось 20, поэтому так и написал.


  1. iCpu
    13.01.2017 07:55

    У меня концептуальный вопрос: а почему необходимо обязательно опрашивать в цикле? Разве DXVA2 не поставляет интерфейса с callback'ами?



    1. nikolaynnov
      13.01.2017 11:45

      Если честно, я уже не помню причин. В любом случае сейчас будем всё напрямую на Intel Media SDK переписывать (благо поддержку аналогичных чипов у nvidia и amd выкинули в силу их глюконутости), там всё по другому будет.


      1. nikolaynnov
        13.01.2017 11:54

        Вот думаю сейчас, что это связано с тем, что мы ещё и аппаратное скалирование делаем в случае необходимости, пока расжатый кадр находится в видеопамяти.


  1. erty
    13.01.2017 11:46

    Занимательнейшее исследование. Спасибо!


  1. alhel
    13.01.2017 11:46

    Частота таймера, вроде, зависит не от версии винды, а от того если какое-либо приложение затребует его уменьшения https://habrahabr.ru/company/intel/blog/186998/


    1. nikolaynnov
      13.01.2017 11:48

      Да, в конце я к этому плавно подвёл. Атак я говорил про дефолтные тайминги, с которыми может столкнуться разработчик при написании/тестировании. А в реальности, да, работает в фоне какое другое приложение и всё, текущее разрешение таймера может быть любым.


      1. mayorovp
        13.01.2017 12:40

        Читал я где-то про wokraround — "чтобы приложение XXX тупило меньше, запустите в фоне Media Player".


        1. nikolaynnov
          13.01.2017 17:19
          +1

          Нашёл.

          gag_fenix:

          Помню, люди, которые держали игровые сервера Counter-Strike на Windows, запускали специально Windows Media Player на сервере, чтобы увеличить tickrate и уменьшить «лагучесть» в игре (-:


  1. vehar
    13.01.2017 11:48

    Отлично! Как раз недавно мудрил нечто подобное


  1. LibertyPaul
    13.01.2017 11:48

    У меня два вопроса к автору:
    1. Почему вместо «минимально возможной паузы», Sleep(1) и прочего не использоавать std::this_thread::yield в комбинации с назначением максимального приоритета процессу?
    2. Почему для декодирования 60 видеопотоков вы используете Windows?


    1. nikolaynnov
      13.01.2017 12:19
      +1

      1) std::this_thread::yield аналогичен Sleep(0) и SwitchToThread. Выше на этот вопрос уже ответили.
      2) А что не так с Windows? И задача была не 60, а 200 видеопотоков расжимать. Кстати, сейчас под линуксом тоже делаем аппаратный декомпрессор, пока его не удалось заставить более 16 потоков расжимать. Правда при этом задача, что нельзя ни драйвера никакие ставить дополнительные, ни чужой софт.


    1. mayorovp
      13.01.2017 12:42

      std::this_thread::yield в комбинации с назначением максимального приоритета процессу приведут к тому, что два таких процесса полностью сожрут одно ядро!


  1. molnij
    13.01.2017 16:01
    +1

    На последнем CLRium у Акиньшина был хороший доклад по таймерам https://youtu.be/4cLoDWoevgU?t=1119
    Не могу сказать, что он бы помог именно в этой теме, но для полноты картины, имхо, очень даже ок


  1. maniacscientist
    13.01.2017 23:44

    Такое ощущение что любой линукс только тем и занимается что перекодирует видео в 200 асинхронных потоков. 500-4000 wakeups — это норма (с) Малышева


  1. vlanko
    14.01.2017 19:21

    А вам не мешает такой большой разброс цифр «сна»?


    1. nikolaynnov
      15.01.2017 01:15

      Вроде нет. Среднее время всё равно около этих 500 микросекунд. Т.е. за секунду каждое ядро может опрашивать (и в 99% после этого выгребсти расжатый кадр) аппаратный декомпрессор 2000 раз. Всего 8000 раз в секунду в среднем на процессор выходит. Мы такой поток кадров не можем в него дать.


  1. johnfound
    15.01.2017 17:30
    -1

    Использование каких либо таймеров – это всегда костыль.


  1. iG0Lka
    19.01.2017 23:35
    -2

    Дайте пожалуйста программу которая сама при запуске устанавливает системный таймер в 500мкс.