我在Haskell库Repa中定义了一个累积和函数。但是,将此功能与转置操作结合使用时遇到了一个问题。以下所有三个操作都可以在不到一秒钟的时间内完成:
cumsum $ cumsum $ cumsum x
transpose $ transpose $ transpose x
transpose $ cumsum x
但是,如果我写:
cumsum $ transpose x
性能急剧下降。虽然每个单独的操作在1920x1080的图像上花费不到一秒钟的时间,但结合起来现在需要30秒钟以上...
关于什么可能导致此的任何想法?我的直觉告诉我,这与延迟数组有关,没有在正确的时间强制执行,等等。但是我还没有足够的经验来追踪这种情况。
{-# LANGUAGE TypeOperators, FlexibleContexts, TypeFamilies #-}
import Data.Array.Repa as Repa
{-# INLINE indexSlice #-}
indexSlice :: (Shape sh, Elt a) => Int -> Array (sh :. Int) a -> (sh :. Int) -> a
indexSlice from arr (z :. ix) = arr `unsafeIndex` (z :. (ix + from))
{-# INLINE sliceRange #-}
sliceRange :: (Slice sh, Shape sh, Elt a) => Int -> Int -> Array (sh :. Int) a -> Array (sh :. Int) a
sliceRange from to arr = fromFunction (z :. (to - from + 1)) $ indexSlice from arr
where (z :. _) = extent arr
{-# INLINE cumsum' #-}
cumsum' :: (Slice (SliceShape sh), Slice sh, Shape (FullShape sh), Shape (SliceShape sh), Elt a, Num a) =>
Array (FullShape sh :. Int) a -> t -> (sh :. Int) -> a
cumsum' arr f (sh :. outer) = Repa.sumAll $ sliceRange 0 outer $ Repa.slice arr (sh :. All)
{-# INLINE cumsum #-}
cumsum :: (FullShape sh ~ sh, Slice sh, Slice (SliceShape sh), Shape sh, Shape (SliceShape sh), Elt a, Num a) =>
Array (sh :. Int) a -> Array (sh :. Int) a
cumsum arr = Repa.force $ unsafeTraverse arr id $ cumsum' arr
最佳答案
从库实现者的角度来看,调试此方法的方法是为可疑操作创建包装器,然后查看核心代码以查看融合是否有效。
-- Main.hs ---------------------------------------------------
import Solver
import Data.Array.Repa.IO.BMP
main
= do Right img <- readImageFromBMP "whatever.bmp"
print $ cumsumBMP img
-- Solver.hs --------------------------------------------------
{-# LANGUAGE TypeOperators, FlexibleContexts, TypeFamilies #-}
module Solver (cumsumBMP) where
import Data.Array.Repa as Repa
import Data.Word
{- all your defs -}
{-# NOINLINE cumsumBMP #-}
cumsumBMP :: Array DIM3 Word8 -> Array DIM3 Word8
cumsumBMP img = cumsum $ transpose img
我已经将“求解器”代码放在单独的模块中,因此我们只需要遍历核心代码即可了解我们关心的定义。
像这样编译:
touch Solver.hs ; ghc -O2 --make Main.hs \
-ddump-simpl -dsuppress-module-prefixes -dsuppress-coercions > dump
转到
cumsumBMP
的定义,并搜索letrec
关键字。搜索letrec
是查找内部循环的快速方法。不太远,我看到了这一点:(略有重新格式化)
case gen_a1tr
of _ {
GenManifest vec_a1tv ->
case sh2_a1tc `cast` ... of _ { :. sh3_a1iu sh4_a1iv ->
case ix'_a1t9 `cast` ... of _ { :. sh1'_a1iz sh2'_a1iA ->
case sh3_a1iu `cast` ... of _ { :. sh5_X1n0 sh6_X1n2 ->
case sh1'_a1iz `cast` ... of _ { :. sh1'1_X1n9 sh2'1_X1nb ->
case sh5_X1n0 of _ { :. sh7_X1n8 sh8_X1na ->
...
case sh2'1_X1nb of _ { I# y3_X1nO ->
case sh4_a1iv of _ { I# y4_X1nP ->
case sh2'_a1iA of _ { I# y5_X1nX ->
...
let { x3_a1x6 :: Int# [LclId]
x3_a1x6 =
+#
(*#
(+#
(*#
y1_a1iM
y2_X1nG)
y3_X1nO)
y4_X1nP)
y5_X1nX } in
case >=#
x3_a1x6
0
of ...
灾害!
x3_a1x6
绑定(bind)显然正在做一些有用的工作(乘法,加法等),但是它包装在一系列的拆箱操作中,这些操作也为每次循环迭代执行。更糟糕的是,每次迭代都会拆开数组的长度和宽度(形状),而此信息将始终相同。 GHC应该真正将这些case表达式浮出循环,但还没有。这是Issue #4081 on the GHC trac的一个实例,希望它将很快得到修复。解决方法是将
deepSeqArray
应用于传入数组。这将其值放在顶层(循环之外),这使GHC知道可以进一步提高匹配值。对于cumsumBMP
这样的函数,我们还希望传入的数组已经很明显,因此我们可以为此添加显式的大小写匹配:{-# NOINLINE cumsumBMP #-}
cumsumBMP :: Array DIM3 Word8 -> Array DIM3 Word8
cumsumBMP img@(Array _ [Region RangeAll (GenManifest _)])
= img `deepSeqArray` cumsum $ transpose img
再次编译,内部循环现在看起来更好了:
letrec {
$s$wfoldlM'_loop_s2mW [...]
:: Int# -> Word# -> Word# [...]
$s$wfoldlM'_loop_s2mW =
\ (sc_s2mA :: Int#) (sc1_s2mB :: Word#) ->
case <=# sc_s2mA a_s2ji of _ {
False -> sc1_s2mB;
True ->
$s$wfoldlM'_loop_s2mW
(+# sc_s2mA 1)
(narrow8Word#
(plusWord#
sc1_s2mB
(indexWord8Array#
rb3_a2gZ
(+#
rb1_a2gX
(+#
(*#
(+#
(*#
wild19_X1zO
ipv1_X1m5)
sc_s2mA)
ipv2_X1m0)
wild20_X1Ct)))))
}; } in
这是一个紧密的,尾部递归的循环,仅使用原始操作。只要您使用
-fllvm -optlo-O3
进行编译,就没有理由不会像等效的C程序那样运行。但是,运行时会有一点打h:
desire:tmp benl$ ./Main
Main: Solver.hs:(50,1)-(51,45): Non-exhaustive patterns in function cumsumBMP
这只是提醒我们,我们需要在调用
cumsumBMP
之前强制使用数组。-- Main.hs ---------------------------------------------------
...
import Data.Array.Repa as Repa
main
= do Right img <- readImageFromBMP "whatever.bmp"
print $ cumsumBMP $ Repa.force img
综上所述:
deepSeqArray
和模式匹配goop功能来解决当前GHC的不足。这证明了
上面的
cumsumBMP
函数的最终版本。如果您要修复GHC总部这很快就会将自己作为抄送添加到Issue #4081 on the GHC trac中。修复此问题后,REPA程序将变得更加漂亮。
indexSlice
和 friend 。一般规则是将goop添加到使用force
,fold
或sumAll
的函数中。这些函数实例化对数组数据进行操作的实际循环,即,它们将延迟的数组转换为清单值。 关于haskell - Repa中的转置和累加和的性能不佳,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/6300428/