私はサブセット積を行うコードを書いています: それは要素のリストと (同じ長さの) 標識変数のリストを取ります。積はツリーで計算されます。これは、アプリケーションにとって重要です。各製品は高価であるため、私の目標は、ツリーの各レベルを並行して計算し、連続するレベルを順番に評価することでした。したがって、ネストされた並列処理は行われません。
コード全体のトップレベル近くにある 1 つの関数に repa コードしかありません。subsetProd はモナドではないことに注意してください。
手順:
- リストをペアにまとめます (並列処理なし)
- チャンクされたリストを圧縮します (並列処理なし)
- このリストに製品関数をマップし (Repa マップを使用)、遅延配列を作成します。
- computeP を呼び出してマップを並行して評価する
- Repa の結果をリストに戻す
- 再帰呼び出しを行います (入力の半分のサイズのリストで)
コード:
{-# LANGUAGE TypeOperators, FlexibleContexts, BangPatterns #-}
import System.Random
import System.Environment (getArgs)
import Control.Monad.State
import Control.Monad.Identity (runIdentity)
import Data.Array.Repa as Repa
import Data.Array.Repa.Eval as Eval
import Data.Array.Repa.Repr.Vector
force :: (Shape sh) => Array D sh e -> Array V sh e
force = runIdentity . computeP
chunk :: [a] -> [(a,a)]
chunk [] = []
chunk (x1:x2:xs) = (x1,x2):(chunk xs)
slow_fib :: Int -> Integer
slow_fib 0 = 0
slow_fib 1 = 1
slow_fib n = slow_fib (n-2) + slow_fib (n-1)
testSubsetProd :: Int -> Int -> IO ()
testSubsetProd size seed = do
let work = do
!flags <- replicateM size (state random)
!values <- replicateM size (state $ randomR (1,10))
return $ subsetProd values flags
value = evalState work (mkStdGen seed)
print value
subsetProd :: [Int] -> [Bool] -> Int
subsetProd [!x] _ = x
subsetProd !vals !flags =
let len = (length vals) `div` 2
!valpairs = Eval.fromList (Z :. len) $ chunk vals :: (Array V (Z :. Int) (Int, Int))
!flagpairs = Eval.fromList (Z :. len) $ chunk flags :: (Array V (Z :. Int) (Bool, Bool))
!prods = force $ Repa.zipWith mul valpairs flagpairs
mul (!v0,!v1) (!f0,!f1)
| (not f0) && (not f1) = 1
| (not f0) = v0+1
| (not f1) = v1+1
| otherwise = fromInteger $ slow_fib ((v0*v1) `mod` 35)
in subsetProd (toList prods) (Prelude.map (uncurry (||)) (toList flagpairs))
main :: IO ()
main = do
args <- getArgs
let [numleaves, seed] = Prelude.map read args :: [Int]
testSubsetProd numleaves seed
プログラム全体は、
ghc -Odph -rtsopts -threaded -fno-liberate-case -funfolding-use-threshold1000 -funfolding-keeness-factor1000 -fllvm -optlo-O3
これらの指示に従って、GHC 7.6.2 x64で。
を使用してプログラム(サブセット)を実行しました
$> time ./Test 4096 4 +RTS -sstderr -N4
8秒後:
672,725,819,784 bytes allocated in the heap
11,312,267,200 bytes copied during GC
866,787,872 bytes maximum residency (49 sample(s))
433,225,376 bytes maximum slop
2360 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 1284212 colls, 1284212 par 174.17s 53.20s 0.0000s 0.0116s
Gen 1 49 colls, 48 par 13.76s 4.63s 0.0946s 0.6412s
Parallel GC work balance: 16.88% (serial 0%, perfect 100%)
TASKS: 6 (1 bound, 5 peak workers (5 total), using -N4)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.00s ( 0.00s elapsed)
MUT time 497.80s (448.38s elapsed)
GC time 187.93s ( 57.84s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 685.73s (506.21s elapsed)
Alloc rate 1,351,400,138 bytes per MUT second
Productivity 72.6% of total user, 98.3% of total elapsed
gc_alloc_block_sync: 8670031
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 571398
-N パラメータを増やすとコードが遅くなります (-N1 の場合は 7.628 秒、-N2 の場合は 7.891 秒、-N4 の場合は 8.659 秒)。並列性が得られません。また、多数の最適化を行ってコンパイルすると、ランタイムには役立ちますが、並列処理には役立ちません。
Threadscope は、3 つの HEC で重大な作業が行われていないことを確認していますが、ガベージ コレクターは 4 つの HEC をすべて使用しているようです。
では、なぜ Repa は火花を散らさないのでしょうか? 私のプロダクト ツリーには 64 枚の葉があるので、Repa が内部ノードごとにスパークを作成したとしても、最大63 個のスパークが存在するはずです。並列処理をカプセル化する ST モナドの使用と関係があるように感じますが、これが問題を引き起こす理由はよくわかりません。おそらく、火花は IO モナドでのみ作成できますか?
この場合、各レベルが並行して行われるこのツリー製品をどのように実行できるかについてのアイデアはありますか (ネストされた並列処理が発生することなく、これは私のタスクには不要と思われます)。一般に、ツリー プロダクトを並列化するか、Repa をより有効に活用するためのより良い方法があると思われます。
スパークが作成されていない場合でも、-N パラメーターを増やすとランタイムが増加する理由を説明するためのボーナス ポイント。
編集 上記のコード例を、私の問題のコンパイル例に変更しました。プログラム フローは、実際のコードとほぼ完全に一致しています。いくつかの入力をランダムに選択し、それらに対してサブセット積を実行します。私は今アイデンティティモナドを使っています。私は自分のコードに多くの小さな変更を試みました: インライン化するかどうか、bang パターンかどうか、2 つの Repa リストと Repa zipWith を使用するバリエーションと、リストを順番に圧縮して Repa マップを使用する方法など、どれもまったく役に立ちませんでした。
サンプル コードでこの問題に遭遇したとしても、実際のプログラムははるかに大きくなります。