19

Haskell ライブラリ Repa で以下に定義する累積合計関数を開発しました。ただし、この関数を転置操作と組み合わせるときに問題が発生しました。次の 3 つの操作はすべて 1 秒未満で完了します。

cumsum $ cumsum $ cumsum x
transpose $ transpose $ transpose x
transpose $ cumsum x

ただし、次のように記述します。

cumsum $ transpose x

パフォーマンスが著しく低下します。1920x1080 の画像では、個々の操作は 1 秒未満で完了しますが、組み合わせると 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
4

1 に答える 1

25

ライブラリの実装者の観点からすると、これをデバッグする方法は、疑わしい操作のラッパーを作成し、コア コードを調べて融合が機能しているかどうかを確認することです。

-- 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有用な作業 (乗算、加算など) を行っていますが、ループの反復ごとに実行される一連の長いボックス化解除操作にラップされています。さらに悪いことに、反復ごとに配列の長さと幅 (形状) をボックス化解除しているため、この情報は常に同じになります。GHC はこれらの case 式をループの外に出す必要がありますが、まだそうではありません。これはGHC trac の問題 #4081のインスタンスであり、近いうちに修正されることを願っています。

回避策は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 プログラムと同じくらい速く実行されない理由はありません。

ただし、実行時にわずかな問題があります。

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

要約すれば:

  1. deepSeqArrayGHC の現在の非効率性を回避するには、いくつかのパターン マッチング グープをトップ レベル関数に追加する必要があります。cumsumBMPこれは、上記の関数の最終バージョンによって実証されています。GHC HQ にこれをすぐに修正してもらいたい場合は、自分自身を CC としてGHC trac の Issue #4081 に追加してください。これが修正されると、Repa プログラムはよりきれいになります。
  2. すべての関数にグープを追加する必要はありません。この例では、私はindexSlice友達に触れる必要はありませんでした。force一般的なルールは、foldまたはを使用する関数にグープを追加することsumAllです。これらの関数は、配列データを操作する実際のループをインスタンス化します。つまり、遅延配列をマニフェスト値に変換します。
  3. Repa コードの一部のパフォーマンスは、実際のコードと同じくらい、それが使用されるコンテキストによって決まります。トップレベルの関数に遅延配列を渡すと、実行が非常に遅くなります。これについては、 The Repa Tutorialでさらに説明しています。
  4. repa-io ライブラリで読み込まれた BMP ファイルは事前に強制されていないため、使用する前に強制する必要があります。これはおそらく間違ったデフォルトなので、次のバージョンで変更します。
于 2011-06-14T08:20:07.710 に答える