Проблема с производительностью: работа с интенсивным использованием ЦП выполняется лучше благодаря большему количеству параллелизма в Erlang - PullRequest
5 голосов
/ 26 июля 2011

ТЛ; др
Я получаю лучшую производительность с моей программой erlang, когда я выполняю задачи с интенсивным использованием ЦП при более высоком параллелизме (например, 10K за раз против 4). Почему?

<ч />

Я пишу каркас сокращения карт, используя erlang, и я делаю тесты производительности.

Моя функция карты сильно загружает процессор (в основном чистый расчет). Ему также требуется доступ к некоторым статическим данным, поэтому у меня есть несколько постоянных (т. Е. Длится, т. Е. Проходит через жизненный цикл приложения) рабочих процессов на моей машине, каждый из которых имеет часть этих данных в памяти и ожидает запросов карты. Выходные данные карты отправляются процессу менеджера (который отправлял запросы на карту рабочим), где выполняется уменьшение (очень легкое).

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

Фрагмент кода:

%% When I remove the comment, I get significant performance boost (90% -> 96%)
%% spawn_link(fun()->
                %% One invocation uses around 250ms of CPU time
                do_map(Map, AssignedSet, Emit, Data),
                Manager ! {finished, JobId, self(), AssignedSet, normal},
%%       end),

По сравнению с тем, когда я выполняю те же вычисления в узком цикле, я получаю 96% -ую пропускную способность (эффективность), используя метод «немедленного появления» (например, 10000 карт сокращают задания, выполняющиеся полностью параллельно). Когда я использую метод «работник выполняет один за другим», я получаю только около 90%.

Я понимаю, что Эрланг, как предполагается, хорош в параллельных вещах, и я впечатлен тем, что эффективность не меняется, даже если я выполняю запросы уменьшения карты на 10 Кб одновременно, в отличие от 100 и т. Д.! Тем не менее, поскольку у меня всего 4 ядра ЦП, я ожидал бы получить более высокую пропускную способность, если бы использовал более низкий параллелизм, например 4 или, возможно, 5.

Странно, но загрузка моего ЦП в двух разных реализациях выглядит очень похоже (почти полностью привязана к 100% на всех ядрах). Разница в производительности довольно стабильна. То есть даже когда я просто выполняю задания по сокращению на 100 карт, я все равно получаю около 96% эффективности с помощью метода «немедленно порождать» и около 90% при использовании метода «один за другим». Аналогично, когда я тестирую с 200, 500, 1000, 10K заданиями.

Сначала я подозревал, что виноват в очереди в очереди рабочих процессов, но даже когда в очереди рабочих процессов должно быть что-то вроде 25 сообщений, я все равно вижу более низкую производительность. 25 сообщений кажутся достаточно маленькими для того, чтобы вызвать засорение (я делаю выборочное сопоставление сообщений, но не так, чтобы процесс должен был помещать сообщения обратно в очередь).

Я не уверен, как мне поступить отсюда. Я делаю что-то не так или я что-то упускаю?

ОБНОВЛЕНИЕ

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

Ответы [ 2 ]

3 голосов
/ 26 июля 2011

Предполагая 1 рабочий процесс с 3 действиями карты, у нас есть первые варианты:

  _______   _______   _______
 |   m   | |   m   | |   m   |
 |       | |       | |       |
_|       |_|       |_|       |_
a         a         a         r

Где a - административные задачи (чтение из очереди сообщений, отправка карты и т. Д.) m - фактическая карта, а r отправляет результат обратно. Второй вариант, где процесс создается для каждой карты:

  _________________._
 |   m              r
 |  ___________________._
 | |   m                r
 | |  _____________________._
_|_|_|   m                  r
a a a

Как видите, административные задачи (a) выполняются в то же время, что и карты (m), и в то же время, что и отправка результатов назад (r).

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

Поскольку у вас довольно высокий уровень параллелизма с самого начала, вы видите лишь относительно небольшое увеличение пропускной способности. Сравните это с теоретически выполненным только одним рабочим процессом (как в первом варианте), вы увидите гораздо большую выгоду.

1 голос
/ 26 июля 2011

Во-первых, позвольте мне заметить, что это очень интересный вопрос. Я хотел бы дать вам несколько советов:

  • Переключение задач происходит в очереди выполнения ([rq: x] в оболочке) из-за сокращения : если процесс Erlang вызывает BIF или пользовательскую функцию, он увеличивает ее редукционный счетчик . При запуске кода с интенсивным использованием ЦП в одном процессе это очень часто увеличивает счетчик сокращений Когда счетчик снижения достигает определенного порога, происходит переключение процесса. (Таким образом, один процесс с более длительным сроком службы имеет такие же накладные расходы, что и несколько процессов с более коротким сроком службы: оба они имеют «один и тот же» счетчик общего сокращения и запускают его, когда он достигает порогового значения, например, один процесс : 50 000 сокращений, больше процессов : 5 * 10 000 сокращений = 50 000 сокращений.) (Причины выполнения)

  • Работа на 4 ядрах против 1 ядра имеет значение: однако, разница во времени. Причина, по которой ваши ядра находятся на 100%, состоит в том, что одно или несколько ядер выполняют / выполняют сопоставление, а другие (-и) эффективно заполняют вашу очередь сообщений. Когда вы создаете отображение, у вас меньше времени на «заполнение» очереди сообщений, больше времени на отображение. Очевидно, что сопоставление является более дорогостоящей операцией, чем заполнение очереди, и предоставление ей большего количества ядер повышает производительность. (Сроки / настройки)

  • Вы получите более высокую пропускную способность при увеличении уровней параллелизма, если процессы ожидают (получение / вызов OTP-серверов и т. Д.). Например: запрос данных у ваших статических постоянных работников занимает некоторое время. (Языковые причины)

...