我有一种算法,可以在给定段上同步计算某个积分。我想使用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
答案 0 :(得分:5)
从外观上,您试图使用梯形法则在f
到b
的区域上近似函数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个线程上运行,或者可以保留为空以使用所有可能的线程。