был открыт.

я есть простая процедура, которая принимает произведение вектораDouble, Я пытаюсь распараллелить этот код, но многие искры заканчиваются провалом. Вот автономный тест, который также предоставляетсякак суть:

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}

{-# OPTIONS_GHC -O2 -Wall -threaded -fforce-recomp #-}

import Criterion.Main
import Control.Monad (when)
import Control.Parallel.Strategies (runEval,rpar,rseq)
import qualified Data.Vector.Primitive as PV

main :: IO ()
main = do
  let expected = PV.product numbers
  when (not (serialProduct numbers == expected)) $ do
    fail "serialProduct implementation incorrect"
  defaultMain
    [ bgroup "product"
      [ bench "serial" $ whnf serialProduct numbers
      , bench "parallel" $ whnf parallelProduct numbers
      ]
    ]

numbers :: PV.Vector Double
numbers = PV.replicate 10000000 1.00000001
{-# NOINLINE numbers #-}

serialProduct :: PV.Vector Double -> Double
serialProduct v =
  let !len = PV.length v
      go :: Double -> Int -> Double
      go !d !ix = if ix < len then go (d * PV.unsafeIndex v ix) (ix + 1) else d
   in go 1.0 0

-- | This only works when the vector length is a multiple of 8.
parallelProduct :: PV.Vector Double -> Double
parallelProduct v = runEval $ do
  let chunk = div (PV.length v) 8
  p2 <- rpar (serialProduct (PV.slice (chunk * 6) chunk v))
  p3 <- rpar (serialProduct (PV.slice (chunk * 7) chunk v))
  p1 <- rseq (serialProduct (PV.slice (chunk * 0) (chunk * 6) v))
  return (p1 * p2 * p3)

Это можно построить и запустить с помощью:

ghc -threaded parallel_compute.hs
./parallel_compute +RTS -N4 -s

У меня 8-ядерный процессор, так что предоставление четырем возможностям должно быть хорошо. Результаты тестов не очень важны, но вот они:

benchmarking product/serial
time                 11.40 ms   (11.30 ms .. 11.53 ms)
                     0.999 R²   (0.998 R² .. 1.000 R²)
mean                 11.43 ms   (11.37 ms .. 11.50 ms)
std dev              167.2 μs   (120.4 μs .. 210.1 μs)

benchmarking product/parallel
time                 10.03 ms   (9.949 ms .. 10.15 ms)
                     0.999 R²   (0.999 R² .. 1.000 R²)
mean                 10.17 ms   (10.11 ms .. 10.31 ms)
std dev              235.7 μs   (133.4 μs .. 426.2 μs)

Теперь статистика выполнения. Вот где я запутался:

   124,508,840 bytes allocated in the heap
   529,843,176 bytes copied during GC
    80,232,008 bytes maximum residency (8344 sample(s))
       901,272 bytes maximum slop
            83 MB total memory in use (0 MB lost due to fragmentation)

                                   Tot time (elapsed)  Avg pause  Max pause
Gen  0        19 colls,    19 par    0.008s   0.001s     0.0001s    0.0003s
Gen  1      8344 colls,  8343 par    2.916s   1.388s     0.0002s    0.0008s

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

TASKS: 13 (1 bound, 12 peak workers (12 total), using -N4)

SPARKS: 1024 (502 converted, 0 overflowed, 0 dud, 28 GC'd, 494 fizzled)

INIT    time    0.000s  (  0.002s elapsed)
MUT     time   11.480s  ( 10.414s elapsed)
GC      time    2.924s  (  1.389s elapsed)
EXIT    time    0.004s  (  0.005s elapsed)
Total   time   14.408s  ( 11.811s elapsed)

Alloc rate    10,845,717 bytes per MUT second

Productivity  79.7% of total user, 88.2% of total elapsed

В разделе, посвященном искрам, мы видим, что примерно половина из них сошла на нет. Это кажется мне невероятным. ВparallelProductу нас основной поток работает над задачей, в 6 раз превышающей то, что дается любому из искр. Тем не менее, кажется, что одна из этих искр всегда падает (или GCed). И это тоже не маленькая работа. Мы говорим о вычислении, которое занимает миллисекунды, поэтому кажется неправдоподобным, что основной поток может завершить его до того, как зажгутся другие блоки.

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

С другой стороны, мыделать увидеть ускорение параллельной версии на 11% в тестах. Итак, восьмая часть успешной работы действительно оказала ощутимое влияние. Мне просто интересно, почему эта другая искра не работает так, как я ожидаю.

Любая помощь в понимании этого будет принята с благодарностью.

РЕДАКТИРОВАТЬ

Я обновилсуть включить другую реализацию:

-- | This only works when the vector length is a multiple of 4.
parallelProductFork :: PV.Vector Double -> Double
parallelProductFork v = unsafePerformIO $ do
  let chunk = div (PV.length v) 4
  var <- newEmptyMVar 
  _ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 0) chunk v)) >>= putMVar var
  _ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 1) chunk v)) >>= putMVar var
  _ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 2) chunk v)) >>= putMVar var
  _ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 3) chunk v)) >>= putMVar var
  a <- takeMVar var
  b <- takeMVar var
  c <- takeMVar var
  d <- takeMVar var
  return (a * b * c * d)

У этого есть превосходная работа:

benchmarking product/parallel mvar
time                 3.814 ms   (3.669 ms .. 3.946 ms)
                     0.986 R²   (0.977 R² .. 0.992 R²)
mean                 3.818 ms   (3.708 ms .. 3.964 ms)
std dev              385.6 μs   (317.1 μs .. 439.8 μs)
variance introduced by outliers: 64% (severely inflated)

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

 Andrew Thaddeus Martin05 окт. 2017 г., 22:56
Извините, я должен был быть более ясным о мотивацииMVar пример. Я немного поиграл с оригинальным примером, прежде чем опубликовать этот вопрос. Делать размер чанка более равномерно распределенным не помогает, потому что по какой-то причине огромное количество искр всегда падает. Когда я делю это равномерно, как я делаю вMVar Например, это составляет около 66% искр, которые теряются, и это та же скорость, что и у серийной версии. Что-то, что я заметил в основанном на искре подходе, - то, что порядок, в котором я умножаю подрезультаты в конце, влияет на процент из них, который терпит неудачу.
 rampion05 окт. 2017 г., 16:59
Что бы это ни стоило, когда я запустил ваш код, в первый раз я получил только 60/604 искр, а 45/554 - второй.
 Andrew Thaddeus Martin05 окт. 2017 г., 16:29
С участием./parallel_compute +RTS -N4 -s -qa -A200M, результаты практически идентичны. Это то, что я подозреваю, так как программа на самом деле не выделяет много.
 jberryman05 окт. 2017 г., 16:16
Ничто не выскакивает на меня, но что произойдет, если вы добавите один / оба-A200M а также-qa к вашим опциям RTS?
 jberryman05 окт. 2017 г., 16:51
Хм, я думаю, что эти статистические данные из критерияmain и, вероятно, не очень полезно (например, критерий вычисляет статистику параллельно в прошлый раз, когда я смотрел). По своему опыту я пришел к недоверию к тому, как среда выполнения и как GC взаимодействует с параллелизмом / параллелизмом с настройками по умолчанию. Наблюдение: выполняя небольшую математику, ваша серийная версия работает примерно так же быстро, как мы надеялись. Это выглядитвозможно что эти два маленьких кусочка, которые вы оцениваете параллельно, просто страдают от плохого поведения кэша, но я не могу заставить цифры там складываться

Ответы на вопрос(1)

Решение Вопроса

что создание искры не приводит к немедленному пробуждению, см.Вот, По умолчанию интервал планирования составляет 20 мс, поэтому, когда вы создаете искру, потребуется до 20 мс, чтобы превратить ее в реальный поток. К тому времени вызывающий поток, скорее всего, уже оценит поток, и искра будет либо GC'd, либо испорчена.

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

Вы можете обойти проблему, уменьшив интервал планирования, используя-C вариант (документы). Например.+RTS -C0.01 кажется достаточно.

 Yuras06 окт. 2017 г., 14:44
@AndrewThaddeusMartin Вероятно, имеет смысл сообщить об ошибке, чтобы узнать, что думают разработчики GHC о планировании зажигания. Интересно, можно ли сразу пробудить способность?
 Andrew Thaddeus Martin06 окт. 2017 г., 14:34
Это решает это. Хотя настройка интервала планирования, подобная этой, может отрицательно повлиять на другие вещи. Поэтому я полагаю, что в обычном случае, если вы собираетесь что-то зажечь, вы должны убедиться, что работа, выполняемая всеми искрами, а также основным потоком, занимает более 20 мс. В противном случае, почти все рухнет, если планирование не произойдет в ближайшее время. Я всегда задавался вопросом о пороге того, какими должны быть мелкозернистые искры, и теперь я понимаю, что это примерно так. Кроме того, где вы нашли документацию-C? Я не могу найти это.
 Yuras06 окт. 2017 г., 14:41
@AndrewThaddeusMartin Я добавил ссылку на документы. По поводу порога - да, это тоже мое впечатление. Хотя я никогда не использовал параллельные стратегии на практике.
 Andrew Thaddeus Martin06 окт. 2017 г., 15:45
Проездной билет14330 был открыт.

Ваш ответ на вопрос