Pull to refresh

Comments 65

А так нельзя сделать?
long t = timer.Microsec();
while((timer.Microsec()-t)<500){}

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

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


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

Не забываем, что так мы нагружаем ядро на полную катушку. Смысл sleep в предоставлении ресурсов другим задачам или, как минимум, выполнении nop. А не в нестандартном приготовлении яичницы и жарке пельменей феном.
Это загрузило бы процессор конкретно, а нам ведь надо с этими расжатыми кадрами ещё много что сделать. Подход на таймерах/слипах позволяет процессору это время выполнять другие потоки.
Интересно, а как select будет себя вести? Не пробовали сравнить?
«разжимали» бы несколько кадров пока ждете миллисекунду.
Проблема в том, что как я сказал у нас синхронный интерфейс декомпрессора. Т.е. сверху нам никто не даст следующий кард, пока мы не разожмём текущий. Собственно, как я уже говорил, у нас был выбор, либо перерабатываем архитектуру, чтобы самим расжимать асинхронно, либо пытаемся сделать меньший слип. Выбрали второе.

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

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

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


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

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

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

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

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

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


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


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


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


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

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

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

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

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


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

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

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

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

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


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

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

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

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


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

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

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

Интересно. Waitable timer'ы используются, это гуд, но кода поднимающего разрешение таймера не видно. Т.е. как показывают тесты, на серверных операционках это спокойно может больше 10 миллисекунд отрабатывать.
Может, SwitchToThread() могла бы здесь справиться лучше таймера?
for (;;) {
  if (frame_is_ready())
    break;
  SwitchToThread();
}

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

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


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

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

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

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

Тема старая, но всё-таки напишу. Я в январе смотрел как работает динамическое повышение приоритета потоков на серверных операционках на примере 2008 R2. Так вот, после WaitForSingleObject приоритет реально как минимум на 1 квант повышается при ожидании евентов и waitable таймера.
SwithToThread просто отдаст остаток кванта времени другому потоку. Но это не поменяет время когда система начнёт планировать потоки на следующий квант. Т.е. это мало будет отличимо от Sleep(1).

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

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

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

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

Насколько я понял (сам в своё время этим же занимался), там не 20мс, а 1/64 с. (т.е. 15-16 мс.)
Да, в реальности 15-16 миллисекунд всегда было. Это и в тестах на Win Server'е видно. Но в разговорной среде почему-то говорилось 20, поэтому так и написал.
У меня концептуальный вопрос: а почему необходимо обязательно опрашивать в цикле? Разве DXVA2 не поставляет интерфейса с callback'ами?
Если честно, я уже не помню причин. В любом случае сейчас будем всё напрямую на Intel Media SDK переписывать (благо поддержку аналогичных чипов у nvidia и amd выкинули в силу их глюконутости), там всё по другому будет.
Вот думаю сейчас, что это связано с тем, что мы ещё и аппаратное скалирование делаем в случае необходимости, пока расжатый кадр находится в видеопамяти.
Занимательнейшее исследование. Спасибо!
Да, в конце я к этому плавно подвёл. Атак я говорил про дефолтные тайминги, с которыми может столкнуться разработчик при написании/тестировании. А в реальности, да, работает в фоне какое другое приложение и всё, текущее разрешение таймера может быть любым.

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

Нашёл.

gag_fenix:
Помню, люди, которые держали игровые сервера Counter-Strike на Windows, запускали специально Windows Media Player на сервере, чтобы увеличить tickrate и уменьшить «лагучесть» в игре (-:
Отлично! Как раз недавно мудрил нечто подобное
У меня два вопроса к автору:
1. Почему вместо «минимально возможной паузы», Sleep(1) и прочего не использоавать std::this_thread::yield в комбинации с назначением максимального приоритета процессу?
2. Почему для декодирования 60 видеопотоков вы используете Windows?
1) std::this_thread::yield аналогичен Sleep(0) и SwitchToThread. Выше на этот вопрос уже ответили.
2) А что не так с Windows? И задача была не 60, а 200 видеопотоков расжимать. Кстати, сейчас под линуксом тоже делаем аппаратный декомпрессор, пока его не удалось заставить более 16 потоков расжимать. Правда при этом задача, что нельзя ни драйвера никакие ставить дополнительные, ни чужой софт.

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

На последнем CLRium у Акиньшина был хороший доклад по таймерам https://youtu.be/4cLoDWoevgU?t=1119
Не могу сказать, что он бы помог именно в этой теме, но для полноты картины, имхо, очень даже ок
Такое ощущение что любой линукс только тем и занимается что перекодирует видео в 200 асинхронных потоков. 500-4000 wakeups — это норма (с) Малышева
А вам не мешает такой большой разброс цифр «сна»?
Вроде нет. Среднее время всё равно около этих 500 микросекунд. Т.е. за секунду каждое ядро может опрашивать (и в 99% после этого выгребсти расжатый кадр) аппаратный декомпрессор 2000 раз. Всего 8000 раз в секунду в среднем на процессор выходит. Мы такой поток кадров не можем в него дать.

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

Дайте пожалуйста программу которая сама при запуске устанавливает системный таймер в 500мкс.
Решение с таймерами всё-равно не идеально (какое-то время мы будем ждать, даже когда кадр уже готов). Даже если интерфейс не предоставляет колбеков (а я ума не приложу как такое может быть), можно было бы влезть чуть глубже и повесить хук на какую-то внутреннюю функцию (это ж стандартные библиотеки винды, их вполне можно разобрать и исследовать). Работало бы одинаково на всех ОС и время бестолкового ожидания свелось бы к нулю.
Sign up to leave a comment.

Articles