Закон Амдала против '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, оно подходит гораздо лучше. А вот если при обработке а) бывают ошибки б) хочется уметь приостанавливать/прекращать - то жизнь становится поинтереснее, правда?