Не удается наблюдать ускорение параллельного кода на Haskell, но профилирование указывает на иное - PullRequest
1 голос
/ 08 мая 2019

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

  1. разделить список пополам
  2. оцените две половины параллельно
  3. объединить их
  4. свернуть список

Мой код:

-- in file par.hs

module Par where

import Control.Parallel
import Data.List

parmap f l = let
    halflen = floor $ (realToFrac $ length l) / 2
    half1 = take halflen l
    half2 = drop halflen l
    mapped1 = map f half1
    mapped2 = map f half2
    in mapped1 `par` (mapped2 `pseq` mapped1 ++ mapped2)

forceEval x = sin x + cos x - tan x + sin(3*x) - cos(2*x)

runMap :: Double
runMap = foldl' (+) 0.0 $ parmap forceEval [0.0..2000000.0]

main = do
    putStrLn $ show runMap

Я скомпилировал это с ghc -prof -fprof-auto -rtsopts -threaded par.hs

Я запустил его с ./par +RTS -p -N?, где ? - количество процессоров

Затем я посмотрел на сгенерированный файл par.prof.


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

Запуск с -N1 дал мне:

    Tue May  7 23:20 2019 Time and Allocation Profiling Report  (Final)

       par +RTS -N1 -p -RTS

    total time  =        1.20 secs   (1200 ticks @ 1000 us, 1 processor)
    total alloc = 1,936,132,144 bytes  (excludes profiling overheads)

COST CENTRE   MODULE    SRC                   %time %alloc

forceEval     Main      par.hs:28:1-57         75.8   60.3
runMap        Main      par.hs:31:1-59         17.5   19.0
parmap'       Main      par.hs:(6,1)-(12,56)    3.2   14.9
parmap'.half1 Main      par.hs:8:5-26           2.3    5.8
parmap'.half2 Main      par.hs:9:5-26           1.1    0.0

Запуск с -N2 (обратите внимание, что профиль показывает ускорение более чем в 2 раза?):

    Tue May  7 23:24 2019 Time and Allocation Profiling Report  (Final)

       par +RTS -N2 -p -RTS

    total time  =        0.36 secs   (725 ticks @ 1000 us, 2 processors)
    total alloc = 1,936,149,368 bytes  (excludes profiling overheads)

COST CENTRE   MODULE    SRC                   %time %alloc

forceEval     Main      par.hs:28:1-57         70.6   60.3
runMap        Main      par.hs:31:1-59         19.3   19.0
parmap'       Main      par.hs:(6,1)-(12,56)    4.3   14.9
parmap'.half1 Main      par.hs:8:5-26           3.3    5.8
parmap'.half2 Main      par.hs:9:5-26           1.7    0.0

Бег с -N4 (обратите внимание на немного большее ускорение по сравнению с -N2):

    Tue May  7 23:25 2019 Time and Allocation Profiling Report  (Final)

       par +RTS -N4 -p -RTS

    total time  =        0.27 secs   (1098 ticks @ 1000 us, 4 processors)
    total alloc = 1,936,183,704 bytes  (excludes profiling overheads)

COST CENTRE   MODULE    SRC                   %time %alloc

forceEval     Main      par.hs:28:1-57         71.5   60.3
runMap        Main      par.hs:31:1-59         19.3   19.0
parmap'       Main      par.hs:(6,1)-(12,56)    3.8   14.9
parmap'.half1 Main      par.hs:8:5-26           3.6    5.8
parmap'.half2 Main      par.hs:9:5-26           1.2    0.0

Я ожидал увидеть некоторое ускорение при работе на двух процессорах, но никакого дополнительного ускорения, если бы я работал на большем количестве процессоров. Но я вообще не мог наблюдать какое-либо ускорение, но приведенные выше профили GHC говорили мне, что было ускорение - нереально хорошее, плюс дополнительное ускорение, если я использую более двух процессоров?

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

Я был бы очень признателен, если бы кто-нибудь объяснил мне, что происходит: это правильный способ написания параллельного кода на Haskell? Почему работа с 4 ядрами может быть полезной во время выполнения, если у меня было только две части для параллельной оценки? Или я просто неверно истолковал вывод профилирования?

Заранее спасибо за любую помощь.

1 Ответ

2 голосов
/ 08 мая 2019

Когда par вычисляет свой первый аргумент, он просто принудительно вычисляет узел верхнего уровня, но он все равно может ссылаться на некоторые ленивые вычисления.

Если вы измените его на

import Control.Parallel
import Data.List

parmap f a l = let
    halflen = floor $ (realToFrac $ length l) / 2
    half1 = take halflen l
    half2 = drop halflen l
    mapped1 = map f half1
    mapped2 = map f half2
    agg1 = a mapped1
    agg2 = a mapped2
    in agg1 `par` agg2 `pseq` a [agg1, agg2] 

forceEval x = sin x + cos x - tan x + sin(3*x) - cos(2*x)                   

runMap :: Double
runMap = parmap forceEval (foldl' (+) 0.0) [0.0..20000000.0]

main = do
    putStrLn $ show runMap

Вы увидите ускорение при переключении с одного потока

time ./f +RTS -N1
-4615093.834471449

real    0m13.077s
user    0m12.333s
sys 0m0.744s

в две нити

time ./f +RTS -N2
-4615093.834471449

real    0m9.057s
user    0m14.512s
sys 0m2.170s

В моем примере agg1 и agg2 являются примитивными значениями, и их легче вычислить.

Alternative

Вы можете использовать rnf из Control.DeepSeq для принудительного вычисления всего списка.

import Control.Parallel 
import Control.DeepSeq
import Data.List

parmap f l = let
    halflen = floor $ (realToFrac $ length l) / 2
    half1 = take halflen l
    half2 = drop halflen l
    mapped1 = map f half1
    mapped2 = map f half2
    in (rnf mapped1) `par` (rnf mapped2) `pseq` (mapped1 ++ mapped2)

forceEval x = sin x + cos x - tan x + sin(3*x) - cos(2*x)

runMap :: Double
runMap = foldl' (+) 0.0 $ parmap forceEval [0.0..20000000.0]

main = do
    putStrLn $ show runMap

И увидите улучшение производительности.

Одна нить

time ./f2 +RTS -N1
-4615093.83447202

real    0m15.241s
user    0m14.261s
sys 0m0.980s

Две темы

time ./f2 +RTS -N2
-4615093.83447202

real    0m11.640s
user    0m17.092s
sys 0m3.178s
...