Я написал минимальный пример, пытаясь распараллелить некоторые вычисления в Haskell. Я в основном пытался отобразить некоторые функции триггера в длинный список с помощью
- разделить список пополам
- оцените две половины параллельно
- объединить их
- свернуть список
Мой код:
-- 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 ядрами может быть полезной во время выполнения, если у меня было только две части для параллельной оценки? Или я просто неверно истолковал вывод профилирования?
Заранее спасибо за любую помощь.