Закон Амдала против 'interrupt rate'

Давно не писал о программировании, а тут вдруг повод появился.

Допустим, мы хотим на Qt в много потоков что-то фигачить. Ну, к примеру, читать метаданные файлов (или метаданные RAW-снимков из RAW-файлов). Понятно, что в реальной жизни это все упирается в диск, но если все уже было прочитано и закэшировано, то нет.

В реальной жизни прекрасно работает вот такой вот паттерн: запустим N threads и в каждой из них будем делать как-то так:

while(job = nextJob()){ result = processJob(job); emit processed(result); }

При этом nextJob() - это какая-то очередь, закрытая к примеру через QMutex.

Если processJob() - очень дешевая (я в тестах делал просто: QString job; result = job.toUpper()), то мы, в первом приближении, померяем собственно скорость работы Qt signal/slot machine в случае многопоточного варианта (т.е. Qt::QueuedConnection).

И вот что получается в результате:

На графике - суммарная производительность всех threads в запросах в секунду (а не одного потока, как можно было бы подумать), то есть для очень дешевой задачи - потери на IPC не просто не дают выигрыша, а чудовищно его съедают.

Разница между новым Qt (5.12) и старым (5.4) есть, для двух потоков вообще процентов 40, но общая картина одинаковая.

Это машина на i9-7960x, т.е. 16 физических ядер/32 если считать HTT.

Для каждого количества потоков делалось 4 замера, на графике - усредненное. Разброс между замерами, на самом деле довольно большой (надо было брать тест еще побольше, но уж сделал), но усреднение приводит к гладкой картине.

Профайлинг показывает, что все упирается в Mutex: WaitForSingleObjectEx в каждом из рабочих потоков:

И, в меньшей степени, в такой же лок в приеме сигналов в главном потоке:

То есть дело не в nextJob(), тамошний QMutex в профиле вообще не видать, из небольшого списка (ну там 100к элементов) выбор задачи происходит мгновенно (я, собственно, давеча это проверил: замена QMutex + QList.takeFirst() на модную lock-free очередь НЕ МЕНЯЕТ НИЧЕГО). А уперто все именно в Qt event loop.

Мораль понятна: ну не приспособлен Qt-шный signal/slot для доставки сотен тысяч сообщений в секунду в многопоточном случае. Из одного потока могет, из двух - могет гораздо хуже, а дальше вообще катастрофа.

Что делать - ну тоже (кажется) понятно:

a) можно, вообще почти не меняя приложения, просто уменьшить rate: накапливать результаты в каком-то буфере (списке) и делать emit пачкой. Сделаю - расскажу, ожидаю большого эффекта.

б) ну или вообще убрать emit processed, а неблокируюшую очередь присобачить там на выходе. Должно стать еще лучше.

P.S. Понятно что если каждый поток обрабатывает, скажем, первые сотни запросов в секунду (что ближе к реальности), то весь этот оверхед не очень виден.

P.P.S. Я знаю про map/filter/reduce и, конечно, для данной задачи, привести сотню тысяч строк к toupper, оно подходит гораздо лучше. А вот если при обработке а) бывают ошибки б) хочется уметь приостанавливать/прекращать - то жизнь становится поинтереснее, правда?

Add new comment