Подтвердить что ты не робот

Как уменьшить накладные расходы на рассылку Haskell?

Я пытаюсь понять производительность рассылки Haskell.

У меня длинный список (длинa > 1000), который я оцениваю параллельно, используя параллельный parMap.

Вот полный вывод статистики с использованием +RTS -s для одного потока (EDIT: полная статистика):

        54,248,802,288 bytes allocated in the heap
           324,451,424 bytes copied during GC
             2,970,272 bytes maximum residency (4 sample(s))
                52,064 bytes maximum slop
                   217 MB total memory in use (1 MB lost due to fragmentation)

                                          Tot time (elapsed)  Avg pause  Max pause
        Gen  0       251 colls,     0 par    1.45s    1.49s     0.0059s    0.0290s
        Gen  1         4 colls,     0 par    0.03s    0.05s     0.0125s    0.0319s

        TASKS: 4 (1 bound, 3 peak workers (3 total), using -N1)

        SPARKS: 6688 (0 converted, 0 overflowed, 0 dud, 1439 GC'd, 5249 fizzled)

        INIT    time    0.00s  (  0.03s elapsed)
        MUT     time   19.76s  ( 20.20s elapsed)
        GC      time    1.48s  (  1.54s elapsed)
        EXIT    time    0.00s  (  0.00s elapsed)
        Total   time   21.25s  ( 21.78s elapsed)

        Alloc rate    2,745,509,084 bytes per MUT second

        Productivity  93.0% of total user, 90.8% of total elapsed

      gc_alloc_block_sync: 0
      whitehole_spin: 0
      gen[0].sync: 0
      gen[1].sync: 0

Если я запускаю два потока, используя +RTS -N2, я получаю:

        54,336,738,680 bytes allocated in the heap
           346,562,320 bytes copied during GC
             5,437,368 bytes maximum residency (5 sample(s))
               120,000 bytes maximum slop
                   432 MB total memory in use (0 MB lost due to fragmentation)

                                          Tot time (elapsed)  Avg pause  Max pause
        Gen  0       127 colls,   127 par    2.07s    0.99s     0.0078s    0.0265s
        Gen  1         5 colls,     4 par    0.08s    0.04s     0.0080s    0.0118s

        Parallel GC work balance: 41.39% (serial 0%, perfect 100%)

        TASKS: 6 (1 bound, 5 peak workers (5 total), using -N2)

        SPARKS: 6688 (6628 converted, 0 overflowed, 0 dud, 0 GC'd, 60 fizzled)

        INIT    time    0.00s  (  0.01s elapsed)
        MUT     time   25.31s  ( 13.35s elapsed)
        GC      time    2.15s  (  1.03s elapsed)
        EXIT    time    0.01s  (  0.01s elapsed)
        Total   time   27.48s  ( 14.40s elapsed)

        Alloc rate    2,146,509,982 bytes per MUT second

        Productivity  92.2% of total user, 175.9% of total elapsed

      gc_alloc_block_sync: 19922
      whitehole_spin: 0
      gen[0].sync: 1
      gen[1].sync: 0

и по четырем потокам:

        54,307,370,096 bytes allocated in the heap
           367,282,056 bytes copied during GC
             8,561,960 bytes maximum residency (6 sample(s))
             3,885,784 bytes maximum slop
                   860 MB total memory in use (0 MB lost due to fragmentation)

                                          Tot time (elapsed)  Avg pause  Max pause
        Gen  0        62 colls,    62 par    2.45s    0.70s     0.0113s    0.0179s
        Gen  1         6 colls,     5 par    0.20s    0.07s     0.0112s    0.0146s

        Parallel GC work balance: 40.57% (serial 0%, perfect 100%)

        TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)

        SPARKS: 6688 (6621 converted, 0 overflowed, 0 dud, 3 GC'd, 64 fizzled)

        INIT    time    0.01s  (  0.01s elapsed)
        MUT     time   37.26s  ( 10.95s elapsed)
        GC      time    2.65s  (  0.77s elapsed)
        EXIT    time    0.01s  (  0.01s elapsed)
        Total   time   39.94s  ( 11.76s elapsed)

        Alloc rate    1,457,427,453 bytes per MUT second

        Productivity  93.4% of total user, 317.2% of total elapsed

      gc_alloc_block_sync: 23494
      whitehole_spin: 0
      gen[0].sync: 10527
      gen[1].sync: 38

Таким образом, согласно прошедшему времени (последнее число в каждом выходе), с двумя ядрами программа занимает ~ 66% однопоточной версии, а с четырьмя ядрами она занимает 54% времени. Это ускорение не так уж плохо, но намного хуже, чем теоретически ожидаемое линейное улучшение с количеством ядер, что приведет к 25% времени выполнения с четырьмя ядрами.

Теперь, смотря на приведенные выше статистические выходы, я вижу, что фактическое рабочее время процессора для программы (строки, начинающиеся с MUT) значительно увеличивается с использованием большего количества ядер. С 1, 2 и 4 ядрами я получаю процессорное время 19.76s, 25.31s и 37.26s, и это увеличение - это то, что, как я полагаю, - потребление моей производительности параллелизации.

Типичные причины для таких издержек времени выполнения процессора с несколькими ядрами, которые мне приходят в голову:

  • слишком тонкая гранулярность распределения рабочей нагрузки. Тем не менее, я пробовал ту же программу, используя parListChunked из пакета parallel с размером блока 10, но результат очень схож, поэтому я не думаю, что накладные расходы обусловлены слишком тонкой детализацией.
  • Сбор мусора: это был большой убийца производительности для моего кода в прошлом, но поскольку я увеличил размер GC до 100 Мб, общее время, проведенное в GC, довольно мало, как видно из статистики выше.

Каковы другие причины столь сильных накладных расходов и как их смягчить?

4b9b3361

Ответ 1

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

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

Предположим, что предел находится где-то между 50-100Gb в секунду (я не уверен, что это правильный номер, пожалуйста, исправьте меня, если у вас есть лучший.)

Вы выделяете 54 ГБ за 10 секунд (случай -N4), поэтому вы имеете пропускную способность 5 Гбит/с. Он довольно высокий, но обычно это не проблема сама по себе.

Большинство распределений обычно являются короткими, и они являются GC'd, когда область выделения gen0 (питомник) заполнена. По умолчанию размер детской составляет 512 Кбайт, поэтому все распределения происходят в кеше L2. Таким образом, короткие живые данные никогда не войдут в основную память, поэтому почти свободны.

Но вы увеличили размер питомника до 100 Мб. Он не будет соответствовать кэшу L2 и будет перенесен в основную память. Это уже плохой знак.

Ну, 5Gb/sec далек от предела. Но есть причина, по которой вы увеличили размер детского сада - ваши данные недолговечны. Он будет использоваться где-то еще после некоторого запаздывания. Это означает, что этот 54Gb будет загружен из основной памяти обратно в кеши рано или поздно. Таким образом, вы, по крайней мере, имеете пропускную способность 10 Гбит/с.

Это еще далеко, но обратите внимание, что это лучший сценарий - последовательный паттерн доступа к памяти. В действительности вы получаете доступ к памяти в случайном порядке, поэтому одни и те же линии кэша загружаются и выгружаются несколько раз, и вы легко достигаете 100 Гбит/с.

Чтобы устранить эту проблему, вы должны определить, почему наши данные не являются краткосрочными и пытаются это исправить. Если это невозможно, вы можете попытаться увеличить локальность данных и изменить шаблон доступа к памяти, чтобы сделать его последовательным.

Я хотел бы знать, что эксперты по аппаратуре думают о моем наивном объяснении:)