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

Контроль параллельного выполнения

Haskell предоставляет комбинатор par, который ставит в очередь "искру" для возможной оценки параллельно с текущим потоком. Он также предоставляет комбинатор pseq, который заставляет оценивать чистый код в определенном порядке.

Что, по-видимому, не предоставляет Haskell, это способ генерировать несколько искр, а затем ждать, пока все они закончатся. Это довольно тривиально для достижения с явным concurrency, но кажется невозможным с чистыми искрами.

Частично это возможно из-за предполагаемого варианта использования искр. Похоже, что они предназначены для спекулятивной оценки. То есть, выполняя работу, которая может потребоваться, но может и не быть. Следовательно, искры работают только на ядрах, которые в противном случае простаивают.

Однако это не мой случай использования. У меня есть куча результатов, которые, как мне известно, скоро понадобятся. И если я начну пытаться обработать результаты до того, как искры начнут стрелять, я просто закончу однопоточным снова с кучей испуганных искр.

Конечно, из par ждал завершения искры, он не достигнет любого parallelism! Но было бы очень полезно, если бы какой-то способ произвести несколько искр, а затем дождаться их завершения. Я не могу найти способ сделать это, хотя.

Есть ли у кого-нибудь полезные предложения? (За исключением "явно использовать concurrency" ).

4b9b3361

Ответ 1

Самый короткий ответ

Вы не можете.

Короткий ответ

Чтобы дождаться искры до конца, попробуйте оценить, что искра оценивала. Например, если у вас есть выражения a и b, для вычисления a + b вы можете сделать

a `par` b `par` (a + b)

или

a `par` b `pseq` (a + b)

Длительный ответ

Когда вы создаете искру с помощью par, вы сообщаете системе времени выполнения "Мне понадобится это значение позже, поэтому вы должны оценить ее параллельно". Когда вы позже нуждаетесь в значении, либо искра оценивает выражение, либо нет. Если он есть, то thunk будет заменен значением, и поэтому переоценка не будет стоить - это просто выборка значения. Если он не был оценен как искра, то ожидание искры бесполезно - для планирования может потребоваться некоторое время, и ожидание потока будет тратить время. Вместо того, чтобы ждать, вы должны просто оценить выражение самостоятельно. По существу, нет необходимости ждать искры. Вы просто пытаетесь оценить исходное выражение и получить преимущества производительности.

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

<сильные > Примеры

Стандартный пример - распараллеливание чисел Фибоначчи, из последовательного

fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

к параллельному

pfib 0 = 0
pfib 1 = 1
pfib n = l `par` r `pseq` (l + r) where
    l = pfib (n - 1)
    r = pfib (n - 2)

.

Теперь для примера, использующего спекуляцию:

spec :: a -- a guess to the input value
    -> (a -> b) -- a function to tranform the input value
    -> a -- the actual input value - this will require computation
    -> b -- the function applied to the input value
spec guess f input = let speculation = f guess in speculation `par`
    if guess == input
        then speculation
        else f input

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

Другие решения, которые делают вещи более явными

  • monad-par
  • Strategies, которые используют par.
  • Мессинг с IO. Здесь есть много вещей.

Ответ 2

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

{-# LANGUAGE BangPatterns #-}
module Main where

import Control.Parallel

fib :: Int -> Int
fib n
    | n < 1     = 0
    | n == 1    = 1
    | otherwise = fib (n-1) + fib (n-2)

trib :: Int -> Int
trib n
    | n < 1     = 0
    | n < 3     = 1
    | otherwise = trib (n-1) + trib (n-2) + trib (n-3)

data R = R { res1, res2 :: !Int }

main :: IO ()
main = do
    let !r = let a = fib 38
                 b = trib 31
             in a `par` b `pseq` (R a b)
    print $ res1 r
    print $ fib 28
    print $ res2 r

Это сработало:

$ ./spark +RTS -N2 -s
39088169
317811
53798080
          65,328 bytes allocated in the heap
           9,688 bytes copied during GC
           5,488 bytes maximum residency (1 sample(s))
          30,680 bytes maximum slop
               2 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0         1 colls,     0 par    0.00s    0.00s     0.0001s    0.0001s
  Gen  1         1 colls,     1 par    0.00s    0.00s     0.0001s    0.0001s

  Parallel GC work balance: 1.33 (686 / 515, ideal 2)

                        MUT time (elapsed)       GC time  (elapsed)
  Task  0 (worker) :    0.59s    (  0.59s)       0.00s    (  0.00s)
  Task  1 (worker) :    0.00s    (  0.59s)       0.00s    (  0.00s)
  Task  2 (bound)  :    0.59s    (  0.59s)       0.00s    (  0.00s)
  Task  3 (worker) :    0.00s    (  0.59s)       0.00s    (  0.00s)

  SPARKS: 1 (1 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    1.17s  (  0.59s elapsed)
  GC      time    0.00s  (  0.00s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time    1.18s  (  0.59s elapsed)

  Alloc rate    55,464 bytes per MUT second

  Productivity  99.9% of total user, 199.1% of total elapsed

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