如何在此示例中添加并行计算?

时间:2019-05-27 21:44:17

标签: algorithm haskell parallel-processing

我有一种算法,可以在给定段上同步计算某个积分。我想使用Control.Parallel库,或者更确切地说使用par :: a -> b -> b向此算法添加并行计算。 我该怎么办?

integrate :: (Double -> Double) -> Double -> Double -> Double
integrate f a b =
  let
    step     = (b - a) / 1000
    segments = [a + x * step | x <- [0..999]]
    area x   = step * (f x + f (x + step)) / 2
  in sum $ map area segments

2 个答案:

答案 0 :(得分:5)

从外观上,您试图使用梯形法则在fb的区域上近似函数a的积分。您尝试并行化代码是正确的,但是尝试存在一些问题:

  • 首先,您需要一个窃用工作的调度程序才能获得任何好处,因为par不太可能使您加速工作
  • 第二,每个中间点f(x)的实现方式都要计算两次,边界点f(a)f(b)除外

几个月前我需要此功能,因此我将其添加到massiv库:trapezoidRule中,该库可方便地解决上述两个问题,并避免使用列表。

这是一个开箱即用的解决方案,但是它不会自动并行化计算,因为仅在计算数组的一个元素(它被设计用来估计许多区域的积分)

integrate' :: (Double -> Double) -> Double -> Double -> Double
integrate' f a b = trapezoidRule Seq P (\scale x -> f (scale x)) a d (Sz1 1) n ! 0
  where
    n = 1000
    d = b - a

作为健全性检查:

λ> integrate (\x -> x * x) 10 20 -- implementation from the question
2333.3335
λ> integrate' (\x -> x * x) 10 20
2333.3335

这是一个可以自动并行化并避免重复评估的解决方案:

integrateA :: Int -> (Double -> Double) -> Double -> Double -> Double
integrateA n f a b =
  let step = (b - a) / fromIntegral n
      sz = size segments - 1
      segments = computeAs P $ A.map f (enumFromStepN Par a step (Sz (n + 1)))
      area y0 y1 = step * (y0 + y1) / 2
      areas = A.zipWith area (extract' 0 sz segments) (extract' 1 sz segments)
   in A.sum areas

由于列表融合,如果您的解决方案使用列表,则不会进行分配,因此,对于简单的情况,它将非常快。在上述解决方案中,将分配大小为n+1的数组,以促进共享并避免双重功能评估。由于调度不是免费的,因此调度也会带来额外的成本。但是最后,对于真正昂贵的功能和非常大的n,可以在四核处理器上加快〜x3倍的速度。

以下是将高斯函数与n = 100000集成在一起的一些基准:

benchmarking Gaussian1D/list
time                 3.657 ms   (3.623 ms .. 3.687 ms)
                     0.999 R²   (0.998 R² .. 1.000 R²)
mean                 3.627 ms   (3.604 ms .. 3.658 ms)
std dev              80.50 μs   (63.62 μs .. 115.4 μs)

benchmarking Gaussian1D/array Seq
time                 3.408 ms   (3.304 ms .. 3.523 ms)
                     0.987 R²   (0.979 R² .. 0.994 R²)
mean                 3.670 ms   (3.578 ms .. 3.839 ms)
std dev              408.0 μs   (293.8 μs .. 627.6 μs)
variance introduced by outliers: 69% (severely inflated)

benchmarking Gaussian1D/array Par
time                 1.340 ms   (1.286 ms .. 1.393 ms)
                     0.980 R²   (0.967 R² .. 0.989 R²)
mean                 1.393 ms   (1.328 ms .. 1.485 ms)
std dev              263.3 μs   (160.1 μs .. 385.6 μs)
variance introduced by outliers: 90% (severely inflated)

旁注建议。切换到Simpson规则将为您提供更好的近似值。在massiv中可以实现;)

修改

这是一个很有趣的问题,我决定看看在不分配任何数组的情况下实现它会怎样。这是我想出的:

integrateS :: Int -> (Double -> Double) -> Double -> Double -> Double
integrateS n f a b =
  let step = (b - a) / fromIntegral n
      segments = A.map f (enumFromStepN Seq (a + step) step (Sz n))
      area y0 y1 = step * (y0 + y1) / 2
      sumWith (acc, y0) y1 =
        let acc' = acc + area y0 y1
         in acc' `seq` (acc', y1)
   in fst $ A.foldlS sumWith (0, f a) segments

以上方法在常量内存中运行,因为创建的几个数组并不是由内存支持的实际数组,而是延迟数组。折叠累加器周围有一些技巧,我们可以共享结果,从而避免双重功能评估。这导致了惊人的速度:

benchmarking Gaussian1D/array Seq no-alloc
time                 1.788 ms   (1.777 ms .. 1.799 ms)
                     1.000 R²   (0.999 R² .. 1.000 R²)
mean                 1.787 ms   (1.781 ms .. 1.795 ms)
std dev              23.85 μs   (17.19 μs .. 31.96 μs)

上述方法的缺点是它不容易并行化,但并非不可能。拥抱自己,这是一种怪兽,可以在8种功能上运行(硬编码,在我的情况下为4个具有超线程的内核):

-- | Will not produce correct results if `n` is not divisible by 8
integrateN8 :: Int -> (Double -> Double) -> Double -> Double -> Double
integrateN8 n f a b =
  let k = 8
      n' = n `div` k
      step = (b - a) / fromIntegral n
      segments =
        makeArrayR D (ParN (fromIntegral k)) (Sz1 k) $ \i ->
          let start = a + step * fromIntegral n' * fromIntegral i + step
           in (f start, A.map f (enumFromStepN Seq (start + step) step (Sz (n' - 1))))
      area y0 y1 = step * (y0 + y1) / 2
      sumWith (acc, y0) y1 =
        let acc' = acc + area y0 y1
         in acc' `seq` (acc', y1)
      partialResults =
        computeAs U $ A.map (\(y0, arr) -> (y0, A.foldlS sumWith (0, y0) arr)) segments
      combine (acc, y0) (y1, (acci, yn)) =
        let acc' = acc + acci + area y0 y1
         in acc' `seq` (acc', yn)
   in fst $ foldlS combine (0, f a) partialResults

分配的唯一实数数组用于保留partialResults,该数组总共有16个Double元素。速度提升并不那么剧烈,但是仍然存在:

benchmarking Gaussian1D/array Par no-alloc
time                 960.1 μs   (914.3 μs .. 1.020 ms)
                     0.968 R²   (0.944 R² .. 0.990 R²)
mean                 931.8 μs   (900.8 μs .. 976.3 μs)
std dev              129.2 μs   (84.20 μs .. 198.8 μs)
variance introduced by outliers: 84% (severely inflated)

答案 1 :(得分:3)

对于任何map组合,我的默认设置都是通过使用parmap API http://hackage.haskell.org/package/parallel-3.2.2.0/docs/Control-Parallel-Strategies.html#g:7中的Strategies来完成,我将在周围添加一个示例PC。

修改

您将通过以下方式使用parMap,

module Main where


import Control.Parallel.Strategies


main = putStrLn $ show $ integrate f 1.1 1.2


f :: Double -> Double
f x = x

integrate :: (Double -> Double) -> Double -> Double -> Double
integrate f a b =
  let
    step     = (b - a) / 1000
    segments = [a + x * step | x <- [0..999]]
    area x   = step * (f x + f (x + step)) / 2
  in sum $ parMap rpar area segments

然后使用以下代码进行编译:

ghc -O2 -threaded -rtsopts Main.hs并使用RTS + N标志运行以控制并行度./Main +RTS -N -RTS -N可以指定为例如-N6在6个线程上运行,或者可以保留为空以使用所有可能的线程。

相关问题