Конвейер в картинках
Sep. 9th, 2012 03:55 pmНа днях занимался тем, что генерил интересные картинки, долго их созерцал, а по результату созерцания успешно ускорял код, не трогая в нем ни строчки. Теперь вот хочу поделиться.
Есть у меня программа, которая обрабатывает видео и показывает в процессе две картинки - "было" и "стало" (текущий обрабатываемый кадр). Как обычно поставлен такой процесс? Сплиттер читает очередной сжатый кадр из входного файла, декодер распаковывает его в картинку, нужная часть картинки рисуется в окошке "было", кадр как-то обрабатывается (в моем случае увеличивался методом super resolution), нужная часть обработанного кадра рисуется в окошке "стало", дальше компрессор его сжимает, а муксер пишет в выходной файл. Параллельно сплиттер также читает порции звука, и либо сразу отдает муксеру для записи в выходной файл, либо через цепочку декодер-энкодер. Делать все в описанном порядке не слишком эффективно, т.к. ядер обычно несколько, надо бы их загрузить. Для этого в цепочку были добавлены фильтры-параллелайзеры: такой фильтр получает порцию данных, складывает ее в очередь и тут же возвращает управление. В другом потоке он из очереди уже скармливает данные нижестоящему фильтру (работает все это в DirectShow, там все упомянутые компоненты - сплиттеры, кодеки, муксеры и пр. называются фильтрами и соединяются в граф, становясь его вершинами). В результате, как только декодер отдал распакованный кадр, он может сразу заняться распаковкой следующего кадра, а отданный только что кадр будет обрабатываться параллельно. Аналогично, как только кадр обработан, можно сразу заняться обработкой следующего, а сжатием его энкодер будет заниматься в отдельном потоке. Конвейер!
Столкнувшись со странными проседаниями производительности на двухядерном ноутбуке, я решил попристальнее изучить, как на практике работает этот конвейер, и где возникают тормоза. Для этого разные его части стали отмечать основные события в лог, но в текстовом виде его изучать оказалось трудно, поэтому был сделан конвертер таких логов в SVG, получились интересные картинки.
Вот так процесс выглядел на четырехядерной машине с DivX'ом в качестве кодера. На входе был FLV файл, но сплиттер и декодер тут не показаны, т.к. они сами в мой лог ничего не пишут, будучи сторонними компонентами. Время тут идет вниз. Разными цветами обозначены разные кадры. Кадр от декодера попадает на первый граббер, который извлекает часть картинки и запоминает. Он отдает кадр параллелайзеру, тот складывает в очередь. В другом потоке уже (разные потоки обозначены разными цветами фона) кадр обрабатывается фильтром SR и отдается еще одному параллелайзеру. В очередном потоке кадр из очереди параллелайзера попадает на граббер2, который извлекает часть обработанного кадра для показа в окошке, передает его кодеку для сжатия, а тот отдает моей писалке AVI файлов.

DivX сам в лог не пишет, но поскольку с двух сторон от него мои фильтры, то моменты получения и отдачи им кадра известны. Тут все гладко: скорость конвейера естественно ограничена скоростью самого медленного участника (здесь это SR), он работает изо всех сил 100% времени, остальные работают параллельно с ним, периодически отдыхая в ожидании очередной порции данных.
А вот как похожий процесс выглядел на двухядерном ноуте. Входной файл и параметры обработки были те же, но вместо DivX'a использовался XviD.

Ни один участников конвейера не работает 100% времени! Поток обработки половину времени простаивает, ожидая пока прочухается поток сжатия и записи. А тот ведет себя странно: то извлечение части кадра в граббере работает мгновенно, а то занимает порядочное время. И пока оно там тормозит, кодек ничем не занят. Плюс можно видеть, что сам кодек ведет себя странно: некоторые кадры сжимает подолгу, а некоторые прям мгновенно. Зная, что это MPEG4, в котором есть B-кадры (bidirectional, они строятся из разницы предыдущего и следующего за текущим кадров), ясно, что XviD каждый второй кадр тут просто запоминает, а когда получит следующий, тогда уже занимается сжатием их обоих. Сжав два кадра, один он отдает сразу, а второй - в следующий раз, когда получит очередной кадр для запоминания. Но почему так тормозит иногда извлечение части кадра в граббере, это же такая простая операция? Присмотревшись к картинке, можно увидеть: быстро эта операция происходит тогда, когда полученный кадр еще горячий, только что из обработки. А вот если он какое-то время провел в очереди, ожидая, то его обработка в граббере затягивается. Дело в кэше! В одном случае он не успевает покинуть кэш, и все быстро. А в другом случае кэш уже замусоривается кодеками, кадр приходится читать из памяти, отсюда ощутимые задержки.
Так пришло первое решение: нужно поменять местами второй граббер и параллелайзер, тогда извлечение части кадра будет происходить сразу же после обработки, кадр не успеет покинуть кэш. И действительно, стоило поменять порядок, извлечение кадра стало стабильно быстрым. Операция ускорилась, хотя в реализации ее не поменялось ни буквы.

Тут время работы кодека уже некорректно обозначено, т.к. достоверно известен лишь момент окончания его работы над кадром, но не начала. Но даже так видно другую проблему. По-прежнему самое медленное звено цепочки - XviD, и в идеале он должен работать постоянно, а другие части конвейера периодически ожидать, когда он сможет принять очередной кадр. Но тут он не работает все время, часть времени он ждет, пока будет обработан очередной кадр. А почему этот кадр не был обработан раньше, ведь у потока обработчика было на это и время, и сам кадр?
Тут дело вот в чем. Когда какой-нибудь сплиттер, декодер или иной фильтр-обработчик хочет создать очередной сэмпл (порцию данных, кадр в случае видео) он запрашивает под него память у аллокатора, насчет которого он договорился с нижестоящим фильтром (в частности, нижестоящий фильтр может быть рендерером, он предоставит аллокатор, который будет размещать кадры сразу в видеопамяти). Количество буферов у аллокатора строго ограничено. Если запрашивается память, а все буферы заняты, операция блокируется в ожидании освобождения. Именно так устроено ограничение очереди в параллелайзере - путем ограничения аллокатора. Понятно, что если очередь не ограничивать, она быстро может сожрать всю память. Если ее сделать большой, то в памяти будет скапливаться множество несжатых кадров, и в случае длинного конвейера (когда фильтров-обработчиков и паралеллайзеров между ними много) это съест слишком много памяти. Поэтому давным-давно решено было сделать очередь из двух сэмплов: один сэмпл отдается нижележащему фильтру для обработки, второй сэмпл может взять вышележащий фильтр для заполнения, они работают параллельно. Когда нижележащий фильтр закончит обрабатывать сэмпл и будет готов принять следующий, он вернет память аллокатору, и в очередь можно будет положить новый сэмпл. В такой схеме все работает параллельно и без лишнего потребления памяти, но в очереди в ожидании может быть не больше одного кадра. Когда XviD обрабатывает один кадр и сразу же проглатывает второй, дожидавшийся в очереди, очередь внезапно оказывается пустой, и ему приходится ждать, пока вышележащий обработчик произведет новый кадр. А до этого он не мог этим заняться, т.к. оба доступных под кадры буфера уже были заняты - один был у кодека, второй лежал в очереди.
Отсюда пришло второе решение: раз некоторые кодеки ведут себя столь нерегулярно, то очередь кадров для них стоит увеличить, а остальным оставить как было. После увеличения числа буферов во втором параллелайзере до трех получилась такая картина:

Поток обработки заработал на полную катушку, больше никакого лишнего простаивания! Общая скорость на двухядерном ноуте выросла всего на несколько процентов, т.к. есть еще декодер, отнимающий время у ядер, и часть тормозов, похоже, была вызвана им (а может, это кодер начинал тормозить, в любом случае выглядело это как существенное замедление кодера). Но все равно приятно, такой детектив прям развернулся.
Есть у меня программа, которая обрабатывает видео и показывает в процессе две картинки - "было" и "стало" (текущий обрабатываемый кадр). Как обычно поставлен такой процесс? Сплиттер читает очередной сжатый кадр из входного файла, декодер распаковывает его в картинку, нужная часть картинки рисуется в окошке "было", кадр как-то обрабатывается (в моем случае увеличивался методом super resolution), нужная часть обработанного кадра рисуется в окошке "стало", дальше компрессор его сжимает, а муксер пишет в выходной файл. Параллельно сплиттер также читает порции звука, и либо сразу отдает муксеру для записи в выходной файл, либо через цепочку декодер-энкодер. Делать все в описанном порядке не слишком эффективно, т.к. ядер обычно несколько, надо бы их загрузить. Для этого в цепочку были добавлены фильтры-параллелайзеры: такой фильтр получает порцию данных, складывает ее в очередь и тут же возвращает управление. В другом потоке он из очереди уже скармливает данные нижестоящему фильтру (работает все это в DirectShow, там все упомянутые компоненты - сплиттеры, кодеки, муксеры и пр. называются фильтрами и соединяются в граф, становясь его вершинами). В результате, как только декодер отдал распакованный кадр, он может сразу заняться распаковкой следующего кадра, а отданный только что кадр будет обрабатываться параллельно. Аналогично, как только кадр обработан, можно сразу заняться обработкой следующего, а сжатием его энкодер будет заниматься в отдельном потоке. Конвейер!
Столкнувшись со странными проседаниями производительности на двухядерном ноутбуке, я решил попристальнее изучить, как на практике работает этот конвейер, и где возникают тормоза. Для этого разные его части стали отмечать основные события в лог, но в текстовом виде его изучать оказалось трудно, поэтому был сделан конвертер таких логов в SVG, получились интересные картинки.
Вот так процесс выглядел на четырехядерной машине с DivX'ом в качестве кодера. На входе был FLV файл, но сплиттер и декодер тут не показаны, т.к. они сами в мой лог ничего не пишут, будучи сторонними компонентами. Время тут идет вниз. Разными цветами обозначены разные кадры. Кадр от декодера попадает на первый граббер, который извлекает часть картинки и запоминает. Он отдает кадр параллелайзеру, тот складывает в очередь. В другом потоке уже (разные потоки обозначены разными цветами фона) кадр обрабатывается фильтром SR и отдается еще одному параллелайзеру. В очередном потоке кадр из очереди параллелайзера попадает на граббер2, который извлекает часть обработанного кадра для показа в окошке, передает его кодеку для сжатия, а тот отдает моей писалке AVI файлов.

DivX сам в лог не пишет, но поскольку с двух сторон от него мои фильтры, то моменты получения и отдачи им кадра известны. Тут все гладко: скорость конвейера естественно ограничена скоростью самого медленного участника (здесь это SR), он работает изо всех сил 100% времени, остальные работают параллельно с ним, периодически отдыхая в ожидании очередной порции данных.
А вот как похожий процесс выглядел на двухядерном ноуте. Входной файл и параметры обработки были те же, но вместо DivX'a использовался XviD.

Ни один участников конвейера не работает 100% времени! Поток обработки половину времени простаивает, ожидая пока прочухается поток сжатия и записи. А тот ведет себя странно: то извлечение части кадра в граббере работает мгновенно, а то занимает порядочное время. И пока оно там тормозит, кодек ничем не занят. Плюс можно видеть, что сам кодек ведет себя странно: некоторые кадры сжимает подолгу, а некоторые прям мгновенно. Зная, что это MPEG4, в котором есть B-кадры (bidirectional, они строятся из разницы предыдущего и следующего за текущим кадров), ясно, что XviD каждый второй кадр тут просто запоминает, а когда получит следующий, тогда уже занимается сжатием их обоих. Сжав два кадра, один он отдает сразу, а второй - в следующий раз, когда получит очередной кадр для запоминания. Но почему так тормозит иногда извлечение части кадра в граббере, это же такая простая операция? Присмотревшись к картинке, можно увидеть: быстро эта операция происходит тогда, когда полученный кадр еще горячий, только что из обработки. А вот если он какое-то время провел в очереди, ожидая, то его обработка в граббере затягивается. Дело в кэше! В одном случае он не успевает покинуть кэш, и все быстро. А в другом случае кэш уже замусоривается кодеками, кадр приходится читать из памяти, отсюда ощутимые задержки.
Так пришло первое решение: нужно поменять местами второй граббер и параллелайзер, тогда извлечение части кадра будет происходить сразу же после обработки, кадр не успеет покинуть кэш. И действительно, стоило поменять порядок, извлечение кадра стало стабильно быстрым. Операция ускорилась, хотя в реализации ее не поменялось ни буквы.

Тут время работы кодека уже некорректно обозначено, т.к. достоверно известен лишь момент окончания его работы над кадром, но не начала. Но даже так видно другую проблему. По-прежнему самое медленное звено цепочки - XviD, и в идеале он должен работать постоянно, а другие части конвейера периодически ожидать, когда он сможет принять очередной кадр. Но тут он не работает все время, часть времени он ждет, пока будет обработан очередной кадр. А почему этот кадр не был обработан раньше, ведь у потока обработчика было на это и время, и сам кадр?
Тут дело вот в чем. Когда какой-нибудь сплиттер, декодер или иной фильтр-обработчик хочет создать очередной сэмпл (порцию данных, кадр в случае видео) он запрашивает под него память у аллокатора, насчет которого он договорился с нижестоящим фильтром (в частности, нижестоящий фильтр может быть рендерером, он предоставит аллокатор, который будет размещать кадры сразу в видеопамяти). Количество буферов у аллокатора строго ограничено. Если запрашивается память, а все буферы заняты, операция блокируется в ожидании освобождения. Именно так устроено ограничение очереди в параллелайзере - путем ограничения аллокатора. Понятно, что если очередь не ограничивать, она быстро может сожрать всю память. Если ее сделать большой, то в памяти будет скапливаться множество несжатых кадров, и в случае длинного конвейера (когда фильтров-обработчиков и паралеллайзеров между ними много) это съест слишком много памяти. Поэтому давным-давно решено было сделать очередь из двух сэмплов: один сэмпл отдается нижележащему фильтру для обработки, второй сэмпл может взять вышележащий фильтр для заполнения, они работают параллельно. Когда нижележащий фильтр закончит обрабатывать сэмпл и будет готов принять следующий, он вернет память аллокатору, и в очередь можно будет положить новый сэмпл. В такой схеме все работает параллельно и без лишнего потребления памяти, но в очереди в ожидании может быть не больше одного кадра. Когда XviD обрабатывает один кадр и сразу же проглатывает второй, дожидавшийся в очереди, очередь внезапно оказывается пустой, и ему приходится ждать, пока вышележащий обработчик произведет новый кадр. А до этого он не мог этим заняться, т.к. оба доступных под кадры буфера уже были заняты - один был у кодека, второй лежал в очереди.
Отсюда пришло второе решение: раз некоторые кодеки ведут себя столь нерегулярно, то очередь кадров для них стоит увеличить, а остальным оставить как было. После увеличения числа буферов во втором параллелайзере до трех получилась такая картина:

Поток обработки заработал на полную катушку, больше никакого лишнего простаивания! Общая скорость на двухядерном ноуте выросла всего на несколько процентов, т.к. есть еще декодер, отнимающий время у ядер, и часть тормозов, похоже, была вызвана им (а может, это кодер начинал тормозить, в любом случае выглядело это как существенное замедление кодера). Но все равно приятно, такой детектив прям развернулся.
no subject
Date: 2012-09-12 12:27 pm (UTC)Данные представляются в виде мноомерноо массива, массив бьётся на чанки, чанки раскидываются по кластеру, на кластере запускают запросы (что автоматически параллелятся).
Кстати, а ведь при помощи пары простых плагинов к SciDB вы можете получить автоматическую кластеризацию обработки.
Либо один instance + количество worker'ов по числу ядер, либо количество instance'ов по числу ядер, либо вообще distributed обработку делать.
Как идея, интересно?
no subject
Date: 2012-09-12 02:20 pm (UTC)