ライブラリの実装者の観点からすると、これをデバッグする方法は、疑わしい操作のラッパーを作成し、コア コードを調べて融合が機能しているかどうかを確認することです。
-- 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
要約すれば:
deepSeqArray
GHC の現在の非効率性を回避するには、いくつかのパターン マッチング グープをトップ レベル関数に追加する必要があります。cumsumBMP
これは、上記の関数の最終バージョンによって実証されています。GHC HQ にこれをすぐに修正してもらいたい場合は、自分自身を CC としてGHC trac の Issue #4081 に追加してください。これが修正されると、Repa プログラムはよりきれいになります。
- すべての関数にグープを追加する必要はありません。この例では、私は
indexSlice
友達に触れる必要はありませんでした。force
一般的なルールは、fold
またはを使用する関数にグープを追加することsumAll
です。これらの関数は、配列データを操作する実際のループをインスタンス化します。つまり、遅延配列をマニフェスト値に変換します。
- Repa コードの一部のパフォーマンスは、実際のコードと同じくらい、それが使用されるコンテキストによって決まります。トップレベルの関数に遅延配列を渡すと、実行が非常に遅くなります。これについては、 The Repa Tutorialでさらに説明しています。
- repa-io ライブラリで読み込まれた BMP ファイルは事前に強制されていないため、使用する前に強制する必要があります。これはおそらく間違ったデフォルトなので、次のバージョンで変更します。