Сперва напомню, что виндоус (как любая не система реального времени) не гарантирует, что поток (некоторые называют его нить, 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;
}
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'ы. Делаю этот шаг, вдруг кто захочет повторить тесты на более старом компиляторе
#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;
}
0: Elapsed 10578
1: Elapsed 14519
2: Elapsed 14592
3: Elapsed 14625
4: Elapsed 14354
Finished. average time:13733
Пытаемся решить проблему в лоб
Перепишем немного нашу программу. И попытаемся использовать очевидное:
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;
}
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, как на единственную альтернативу. Что же, напишем ещё один хелперный классец.
#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;
}
Посмотрим, изменилось что.
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.
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. В документации Заявляется, что с помощью этой функции можно устанавливать желаемую точностью таймера. Давайте проверим. Ещё раз модифицируем нашу программу.
#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;
}
Традиционно, типичные выводы нашей програмы.
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
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
Давай те разберём интересные факты, которые видны из результатов:
- На windows 8.1 ничего не поменялось. Делаем вывод, что timeBeginPeriod достаточно умный, т.е. если N приложений запросили разрешение системного таймера в разные значения, то понижаться это разрешение не будет. На Windows 7 мы бы тоже не заметили никаких изменений, так как там разрешение таймера уже стоит в 1 мс.
- На серверной операционке, timeBeginPeriod(1) отработал неожиданным образом: он установил разрешение системного таймера в наибольшее возможное значение. Т.е. на таких операционках где-то явно зашит воркараунт вида:
void timeBeginPerion(UINT uPeriod) { if (uPeriod == 1) { setMaxTimerResolution(); return; } ... }
Замечу, что на Windows Server 2003 R2 такого ещё не было. Это нововведение в 2008м сервере.
- На серверной операционке, 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.
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")
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
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
Осталось сделать наблюдения и выводы.
Наблюдения:
- На Win8 после первого запуска программы разрешение системного таймера сбросилось в большое значение. Т.е. вывод 2 был нами сделан неправильно.
- После ручной установки разброс реальных слипов для случая WaitableTimer вырос, хоть в среднем слип и держится около 500 микросекунд.
- На серверной операционке очень неожиданно перестал работать 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)
hacenator
12.01.2017 20:31«разжимали» бы несколько кадров пока ждете миллисекунду.
nikolaynnov
13.01.2017 11:21Проблема в том, что как я сказал у нас синхронный интерфейс декомпрессора. Т.е. сверху нам никто не даст следующий кард, пока мы не разожмём текущий. Собственно, как я уже говорил, у нас был выбор, либо перерабатываем архитектуру, чтобы самим расжимать асинхронно, либо пытаемся сделать меньший слип. Выбрали второе.
mayorovp
13.01.2017 12:36А нельзя параллельно разжимать кадры? Один поток разжимает один кадр, второй поток разжимает другой кадр… Вы же говорили про 200 видеопотоков.
nikolaynnov
13.01.2017 13:43Так так и делаем. Только ядра-то всего 4.
Смотрите, очевидно, что со слипом в 1 миллисекунду, за 1 секунду можно разжать 1000 кадров. Это на одном ядре. При среднем fps равным 25, это всего 40 потоков. Т.е. на 4-х ядерном проце получается всего 160 потоков (4000 кадров в секунду). А цель: 200 потоков, т.е. 200 * 25 = 5000 кадров в секунду.mayorovp
13.01.2017 14:02А вы не пробовали просто запустить параллельно 200 потоков, а не 4?
Да, вы сами не можете проснуться когда преобразование кадра закончилось. Но драйвер-то наверняка это знает! А значит, пока один поток спит свои 16 милисекунд, другие потоки на том же самом ядре смогут делать свою работу.
nikolaynnov
13.01.2017 14:05Пробовали, но большего чип от интела не позволяет. Только 4 потока, иначе они они уже будут ниже синхронизироваться за доступ к аппаратным ресурсам.
mayorovp
13.01.2017 14:07Ну и пусть синхронизируются. Важно лишь, что там ниже у них не будет нижнего ограничения на время сна.
nikolaynnov
13.01.2017 14:19Там возникают другие проблемы. Вплоть до того, что на накладных расходах много теряется. да и просто 200 мегов только на стеки — это уже много. Да и отлаживаться потом с таким кол-вом потоков сложновато будет. Тут пул-потоков — самое очевидное решение.
Да и вообще 25 fps — это 1 кадр в 40 миллисекунд. Т.е. нам надо, чтобы раз в 40 миллисекунд винда нам выделяла хоть немного времени, чтобы мы успели выгребсти результат предыдущего декодирования, вернули его наверх, нам чтобы спустили новую порцию данных, которые мы бы также запихнули в декомпрессор. Предположим, что винда переключает потоки раз в 15 миллисекунд, т.е. за это время на 4-х ядерном проце успее поработать всего 12 потоков. Пусть мы реально быстро делаем подобные операции (выгребсти ...., запихнуть), скажем 1 миллисекунду, и тут же вызываем yield, чтобы остаток кванта отдать другому потоку. (Ну либо точность таймера 1 миллисекунда). В таком случае, успеет поработать 40*4 = 160 потоков. Хм. что-то всё равно не сходится, надо подумать. Вроде как вариант с 200-та потоками работал (но плохо).nikolaynnov
13.01.2017 14:27Вообще, если память мне не изменяет, то на 200-х потоках, мы так и не достигли своей цели, расжималось что-то около 180 потоков (ну т.е. 4500 кадров в секунду, при нужных 5000).
mayorovp
13.01.2017 14:37Вы переоцениваете сложность копирования. Если у вас 4 потока успевали все копировать за отведенное время — то и 200 потоков ту же самую задачу должны успеть выполнить. Суммарный объем-то не поменялся!
И даже частые переключения потоков тут не должны стать проблемой, потому что системные вызовы вы в 4 потока делаете даже чаще чем в 200, а кеши процессора все равно бесполезны в деле копирования больших объемов данных.
Проблему при таком подходе я ожидаю в стабильности. Если в четырех-поточном варианте при мгновенном перегрузе один поток задержится на лишнюю половину миллисекунды — то в варианте с 200 потоками куча потоков задержатся на лишние 16 миллисекунд.
Кроме того, многое зависит от реализации на стороне драйвера. Там внутри тоже может слип на 16 миллисекунд стоять :)
Потому и было интересно пробовали ли и что получилось.
nikolaynnov
13.01.2017 14:49В теории да, должно работать.
В пробовали, но что-то не дотягивали. В итоге вернулись к пулу потоков ик слипу в 0.5 миллисекунды.
tsklab
12.01.2017 21:16-4Но что если мы хотим спать ещё меньше?
HPETDistortNeo
12.01.2017 22:08+2Поясните. С помощью QueryPerformanceCounter и QueryPerformanceFrequency можно точно замерять интервалы — это уже давно всем известно. А вот заставить операционную систему напрямую использовать HPET для вызова кода по таймеру все равно не получится, здесь придётся писать свою операционную систему.
tsklab
12.01.2017 22:45-1DistortNeo
12.01.2017 22:50+1Ну да, Stopwatch — обёртка над функциями QueryPerformanceCounter и QueryPerformanceFrequency из API.
Но как это поможет нам сделать Sleep на полсекунды, мне непонятно.
Разве что busy wait с периодической проверкой таймера. Можно вычисление биткоинов запихать, чтоб процессор совсем вхолостую не работал.
Ambroyz
13.01.2017 12:11речь была не про «полсекунды» и не про «ровно полсекунды», а как спать меньше миллисекунды.
DistortNeo
13.01.2017 13:03+1Почему же? В посте речь идёт именно о полсекунды. Почему полсекунды — да потому что это минимальной возможный интервал системного таймера в Windows. А загвоздка в том, что sleep принимает целое число в миллисекундах, и поэтому приходится использовать платформозависимый API,
tsklab
12.01.2017 22:40-3nikolaynnov
13.01.2017 11:29Если вы внимательно прочитали статью, что для замеров временных интервалов, я так же использую QueryPerformanceCounter'ы.
nckma
12.01.2017 22:53+1Честно говоря не думаю, что использование (любых) слипов — это вообще хорошее решение.
Ожидать событие в цикле со слипом можно только в некритических приложениях. Для целей обработки видео и аудио — это как-то очень не аккуратно.
Вы же сами пишите "Т.е. что-либо утверждать или гарантировать нельзя!". Значит нужно искать решения, где алгоритм будет гарантировать передачу блоков данных точно в нужное время. Наверняка АПИ предполагает какие-то колбэки или события которые можно ждать не в слипах, а скажем в waitforsingleobject(..) или подобных функциях.DistortNeo
12.01.2017 22:58Автор так и написал:
Собственно было 2 варианта: либо переделывать всё на асинхронную работу с аппаратным декомпрессором, либо уменьшать время Sleep'а.
Видимо, костыль в виде второго варианта оказался проще.
nckma
12.01.2017 23:11-1То есть статья про то, как сделать кривой костыль?
DistortNeo
12.01.2017 23:14+2Нет, статья о том, как сделать Sleep на полсекунды в Windows. А то, что применяется как костыль — на то воля программиста.
У меня не было необходимости делать такой точный Sleep, хотя и писал свой планировщик задач для асинхронного выполнения, но, тем не менее, мне это было интересно.
nckma
13.01.2017 08:51Это действительно кажется интересным. Однако, проблема состоит в том, что везде, где якобы требуется такой точный слип его применение окажется костылем и странным архитектурным решением.
mayorovp
13.01.2017 08:54+2В данном случае "странное архитектурное решение" находится внутри DXVA2 и сделать с этим ничего нельзя.
zoonman
12.01.2017 23:10+1Видел в исходниках PHP такое.
nikolaynnov
13.01.2017 11:34Интересно. Waitable timer'ы используются, это гуд, но кода поднимающего разрешение таймера не видно. Т.е. как показывают тесты, на серверных операционках это спокойно может больше 10 миллисекунд отрабатывать.
lexasss
13.01.2017 00:25Может, SwitchToThread() могла бы здесь справиться лучше таймера?
for (;;) { if (frame_is_ready()) break; SwitchToThread(); }
Честно говоря, сам такого никогда не пробовал, не обессудьте если совет в молоко.mayorovp
13.01.2017 08:43+2Нет гарантии, что другой поток достаточно быстро вернет управление обратно. Поток же, который ожидает таймера, при пробуждении получает повышенный приоритет (на клиентских осях).
Также потоку можно явно поставить повышенный или высокий приоритет. В таком случае таймер будет будить его сразу же при срабатывании, а вот SwitchToThread() просто перестанет работать как задумывалось.
nikolaynnov
13.01.2017 11:39при пробуждении получает повышенный приоритет (на клиентских осях).
Ой, я был уверен, что на серверных всё так же с динамическим повышением приоритетов, как и в клиентских осях :-(. Надо будет себе на заметку взять провести тесты на эту тему.mayorovp
13.01.2017 12:38На серверных точно знаю что приоритет процесса, отвечающего за активное окно, не повышается. Про другие эвристики когда-то знал, да забыл. Может, они продолжают работать, а может и нет.
nikolaynnov
13.01.2017 11:37SwithToThread просто отдаст остаток кванта времени другому потоку. Но это не поменяет время когда система начнёт планировать потоки на следующий квант. Т.е. это мало будет отличимо от Sleep(1).
ARad
13.01.2017 06:19Использовать Sleep(0) не пробовали? Там выполнение передается потоку по приоритету и может обратно вернуться без ожидания если более приоритетных потоков нет.
mayorovp
13.01.2017 08:49+1Та же проблема что и с SwitchToThread() (см. выше) — нет гарантии что управление вернется обратно быстро, равно как и нет гарантии что оно не вернется сразу же.
ARad
13.01.2017 20:56Выполнение перейдет в другой поток декодирования, и как только у них не станет работы вернется в ваш, и не станет ждать 20 мс. Т.е. оно начнет загружать потоки по полной и нормально распределять приоритеты, так как потоки не будут находиться в очереди таймера, а только в нормальной очереди. И только если система будет перегружена работой, тогда управление будет возвращаться реже, но это и при ожидании таймера будет происходить. Т.е. 0 задержка это передача выполнения наиболее приоритетному потоку с точки зрения ОС.
hdfan2
13.01.2017 07:46+1Есть некий квант времени, выделяемый потоку на выполнение (да, да, те самые 20 мс)
Насколько я понял (сам в своё время этим же занимался), там не 20мс, а 1/64 с. (т.е. 15-16 мс.)nikolaynnov
13.01.2017 11:41Да, в реальности 15-16 миллисекунд всегда было. Это и в тестах на Win Server'е видно. Но в разговорной среде почему-то говорилось 20, поэтому так и написал.
iCpu
13.01.2017 07:55У меня концептуальный вопрос: а почему необходимо обязательно опрашивать в цикле? Разве DXVA2 не поставляет интерфейса с callback'ами?
mayorovp
13.01.2017 08:46iCpu
13.01.2017 10:44Может, я нашёт не то, на что это похоже, а надувной плот жёлтого цвета в форме X, но.
Call IMFTrackedSample::SetAllocator and provide a pointer to the IMFAsyncCallback interface. (The software decoder must implement this interface.) When the video renderer releases the sample, the callback will be invoked. Use this callback to keep track of which samples are currently available and which are in use.
nikolaynnov
13.01.2017 11:45Если честно, я уже не помню причин. В любом случае сейчас будем всё напрямую на Intel Media SDK переписывать (благо поддержку аналогичных чипов у nvidia и amd выкинули в силу их глюконутости), там всё по другому будет.
nikolaynnov
13.01.2017 11:54Вот думаю сейчас, что это связано с тем, что мы ещё и аппаратное скалирование делаем в случае необходимости, пока расжатый кадр находится в видеопамяти.
alhel
13.01.2017 11:46Частота таймера, вроде, зависит не от версии винды, а от того если какое-либо приложение затребует его уменьшения https://habrahabr.ru/company/intel/blog/186998/
nikolaynnov
13.01.2017 11:48Да, в конце я к этому плавно подвёл. Атак я говорил про дефолтные тайминги, с которыми может столкнуться разработчик при написании/тестировании. А в реальности, да, работает в фоне какое другое приложение и всё, текущее разрешение таймера может быть любым.
mayorovp
13.01.2017 12:40Читал я где-то про wokraround — "чтобы приложение XXX тупило меньше, запустите в фоне Media Player".
nikolaynnov
13.01.2017 17:19+1
LibertyPaul
13.01.2017 11:48У меня два вопроса к автору:
1. Почему вместо «минимально возможной паузы», Sleep(1) и прочего не использоавать std::this_thread::yield в комбинации с назначением максимального приоритета процессу?
2. Почему для декодирования 60 видеопотоков вы используете Windows?nikolaynnov
13.01.2017 12:19+11) std::this_thread::yield аналогичен Sleep(0) и SwitchToThread. Выше на этот вопрос уже ответили.
2) А что не так с Windows? И задача была не 60, а 200 видеопотоков расжимать. Кстати, сейчас под линуксом тоже делаем аппаратный декомпрессор, пока его не удалось заставить более 16 потоков расжимать. Правда при этом задача, что нельзя ни драйвера никакие ставить дополнительные, ни чужой софт.
mayorovp
13.01.2017 12:42std::this_thread::yield в комбинации с назначением максимального приоритета процессу приведут к тому, что два таких процесса полностью сожрут одно ядро!
molnij
13.01.2017 16:01+1На последнем CLRium у Акиньшина был хороший доклад по таймерам https://youtu.be/4cLoDWoevgU?t=1119
Не могу сказать, что он бы помог именно в этой теме, но для полноты картины, имхо, очень даже ок
maniacscientist
13.01.2017 23:44Такое ощущение что любой линукс только тем и занимается что перекодирует видео в 200 асинхронных потоков. 500-4000 wakeups — это норма (с) Малышева
vlanko
14.01.2017 19:21А вам не мешает такой большой разброс цифр «сна»?
nikolaynnov
15.01.2017 01:15Вроде нет. Среднее время всё равно около этих 500 микросекунд. Т.е. за секунду каждое ядро может опрашивать (и в 99% после этого выгребсти расжатый кадр) аппаратный декомпрессор 2000 раз. Всего 8000 раз в секунду в среднем на процессор выходит. Мы такой поток кадров не можем в него дать.
iG0Lka
19.01.2017 23:35-2Дайте пожалуйста программу которая сама при запуске устанавливает системный таймер в 500мкс.
HOMPAIN
А так нельзя сделать?
Всё равно же за это время операционка потоки не успеет перераспределить и реально «ждать» необязательно
DistortNeo
Как это не успеет? Напишите простое пинг-понг приложение и посчитайте, сколько раз будет передано управление из одного потока в другой. Увидите от 10 до 100 тысяч переключений в секунду.
Причём результаты будут одинаковы что при использовании сокетов, что при использовании событий. Планировщик не будет ждать очередного кванта времени, допуская простой процессора, если выполнение потока можно продолжить вотпрямщас.
iCpu
Не забываем, что так мы нагружаем ядро на полную катушку. Смысл sleep в предоставлении ресурсов другим задачам или, как минимум, выполнении nop. А не в нестандартном приготовлении яичницы и жарке пельменей феном.
nikolaynnov
Это загрузило бы процессор конкретно, а нам ведь надо с этими расжатыми кадрами ещё много что сделать. Подход на таймерах/слипах позволяет процессору это время выполнять другие потоки.