4

長さLのK個のリストのリストであるリストA=[B]を中間結果として持つ計算を扱っています。Bの要素を計算する時間計算量はパラメーターMによって制御され、次のようになります。 Mでは理論的に線形です。理論的には、Aの計算の時間計算量はO(K * L * M)であると予想されます。しかし、そうではなく、理由がわかりません。

これが私が説明した問題を示す簡単な完全なスケッチプログラムです

import System.Random (randoms, mkStdGen)
import Control.Parallel.Strategies (parMap, rdeepseq)
import Control.DeepSeq (NFData)
import Data.List (transpose)

type Point = (Double, Double)

fmod :: Double -> Double -> Double
fmod a b | a < 0     = b - fmod (abs a) b 
         | otherwise = if a < b then a 
                       else let q = a / b in b * (q - fromIntegral (floor q))

standardMap :: Double -> Point -> Point
standardMap k (q, p) = (fmod (q + p) (2 * pi), fmod (p + k * sin(q)) (2 * pi))

trajectory :: (Point -> Point) -> Point -> [Point] 
trajectory map initial = initial : (trajectory map $ map initial)

justEvery :: Int -> [a] -> [a]
justEvery n (x:xs) = x : (justEvery n $ drop (n-1) xs)
justEvery _ []     = []

subTrace :: Int -> Int -> [a] -> [a]
subTrace n m = take (n + 1) . justEvery m

ensemble :: Int -> [Point]
ensemble n = let qs = randoms (mkStdGen 42)
                 ps = randoms (mkStdGen 21)
             in take n $ zip qs ps 

ensembleTrace :: NFData a => (Point -> [Point]) -> (Point -> a) -> 
                              Int -> Int -> [Point] -> [[a]]
ensembleTrace orbitGen observable n m = 
    parMap rdeepseq ((map observable . subTrace n m) . orbitGen)

main = let k = 100
           l = 100
           m = 100
           orbitGen = trajectory (standardMap 7)
           observable (p, q) = p^2 - q^2
           initials = ensemble k
           mean xs = (sum xs) / (fromIntegral $ length xs)
           result =   (map mean) 
                    $ transpose 
                    $ ensembleTrace orbitGen observable l m 
                    $ initials
       in mapM_ print result

私はコンパイルしています

$ ghc -O2 stdmap.hs -threaded

と実行します

$ ./stdmap +RTS -N4 > /dev/null

Intel Q6600、Linux 3.6.3-1-ARCH、GHC 7.6.1で、パラメーターK、L、M(プログラムのコードではk、l、m)のさまざまなセットに対して次の結果が得られます。

(K=200,L=200,N=200)   -> real    0m0.774s
                         user    0m2.856s
                         sys     0m0.147s

(K=2000,L=200,M=200)  -> real    0m7.409s
                         user    0m28.102s
                         sys     0m1.080s

(K=200,L=2000,M=200)  -> real    0m7.326s
                         user    0m27.932s
                         sys     0m1.020s

(K=200,L=200,M=2000)  -> real    0m10.581s
                         user    0m38.564s
                         sys     0m3.376s

(K=20000,L=200,M=200) -> real    4m22.156s
                         user    7m30.007s
                         sys     0m40.321s

(K=200,L=20000,M=200) -> real    1m16.222s
                         user    4m45.891s
                         sys     0m15.812s

(K=200,L=200,M=20000) -> real    8m15.060s
                         user    23m10.909s
                         sys     9m24.450s

このような純粋なスケーリングの問題がどこにあるのか、私にはよくわかりません。私が正しく理解していれば、リストは怠惰であり、ヘッドテール方向に消費されるため、作成すべきではありませんか?測定から観察できるように、過剰なリアルタイム消費と過剰なシステム時間消費の間には相関関係があります。これは、過剰がシステムアカウントにあるためです。ただし、メモリ管理に時間を浪費する場合でも、K、L、Mで線形にスケーリングする必要があります。

ヘルプ!

編集

ダニエル・フィッシャーの提案に従ってコードを変更しました。これにより、Mに関する悪いスケーリングが実際に解決されました。指摘されたように、軌道の厳密な評価を強制することで、大きなサンクの構築を回避します。その背後にあるパフォーマンスの向上は理解していますが、元のコードのスケーリングが悪いことはまだわかりません。(正しく理解していれば)サンクの構築の時空間計算量はMで線形である必要があるからです。

さらに、K(アンサンブルのサイズ)に関する不適切なスケーリングを理解するのにまだ問題があります。L = 200、M = 200を維持しながら、K=8000とK=16000のコードを改善して2つの追加測定を実行しました。K = 8000までのスケールアップは予想どおりですが、K=16000の場合はすでに異常です。問題はoverflowed SPARKS、K = 8000の場合は0、K=16000の場合は7802の数にあるようです。Q = (MUT cpu time) / (MUT real time)これはおそらく、CPUの数に理想的に等しい商として私が定量化する悪い並行性に反映されています。ただし、K = 8000の場合はQ〜4、K = 16000の場合はQ〜2です。この問題の原因と考えられる解決策を理解するのを手伝ってください。

K = 8000:

$ ghc -O2 stmap.hs -threaded -XBangPatterns
$ ./stmap +RTS -s -N4 > /dev/null

56,905,405,184 bytes allocated in the heap
 503,501,680 bytes copied during GC
  53,781,168 bytes maximum residency (15 sample(s))
   6,289,112 bytes maximum slop
         151 MB total memory in use (0 MB lost due to fragmentation)

                                Tot time (elapsed)  Avg pause  Max pause
Gen  0     27893 colls, 27893 par    7.85s    1.99s     0.0001s    0.0089s
Gen  1        15 colls,    14 par    1.20s    0.30s     0.0202s    0.0558s

Parallel GC work balance: 23.49% (serial 0%, perfect 100%)

TASKS: 6 (1 bound, 5 peak workers (5 total), using -N4)

SPARKS: 8000 (8000 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

INIT    time    0.00s  (  0.00s elapsed)
MUT     time   95.90s  ( 24.28s elapsed)
GC      time    9.04s  (  2.29s elapsed)
EXIT    time    0.00s  (  0.00s elapsed)
Total   time  104.95s  ( 26.58s elapsed)

Alloc rate    593,366,811 bytes per MUT second

Productivity  91.4% of total user, 360.9% of total elapsed

gc_alloc_block_sync: 315819

K = 16000:

$ ghc -O2 stmap.hs -threaded -XBangPatterns
$ ./stmap +RTS -s -N4 > /dev/null

113,809,786,848 bytes allocated in the heap
 1,156,991,152 bytes copied during GC
  114,778,896 bytes maximum residency (18 sample(s))
    11,124,592 bytes maximum slop
      300 MB total memory in use (0 MB lost due to fragmentation)

                                Tot time (elapsed)  Avg pause  Max pause
Gen  0     135521 colls, 135521 par   22.83s    6.59s     0.0000s    0.0190s
Gen  1        18 colls,    17 par    2.72s    0.73s     0.0405s    0.1692s

Parallel GC work balance: 18.05% (serial 0%, perfect 100%)

TASKS: 6 (1 bound, 5 peak workers (5 total), using -N4)

SPARKS: 16000 (8198 converted, 7802 overflowed, 0 dud, 0 GC'd, 0 fizzled)

INIT    time    0.00s  (  0.00s elapsed)
MUT     time  221.77s  (139.78s elapsed)
GC      time   25.56s  (  7.32s elapsed)
EXIT    time    0.00s  (  0.00s elapsed)
Total   time  247.34s  (147.10s elapsed)

Alloc rate    513,176,874 bytes per MUT second

Productivity  89.7% of total user, 150.8% of total elapsed

gc_alloc_block_sync: 814824
4

2 に答える 2

7

MADのポイントfmodは良いものですが、Cに声をかける必要はなく、Haskellの土地にとどまることができます(リンクされたスレッドのチケットは修正されています)。のトラブル

fmod :: Double -> Double -> Double
fmod a b | a < 0     = b - fmod (abs a) b 
         | otherwise = if a < b then a 
                       else let q = a / b in b * (q - fromIntegral (floor q))

floor :: Double -> Integerタイプのデフォルトは(そして結果としてfromIntegral :: Integer -> Double)呼び出されることにつながるということです。現在、Integerは比較的複雑なタイプで、操作が遅く、からIntegerへの変換Doubleも比較的複雑です。元のコード(パラメーターk = l = 200m = 5000)は統計を生成しました

./nstdmap +RTS -s -N2 > /dev/null
  60,601,075,392 bytes allocated in the heap
  36,832,004,184 bytes copied during GC
       2,435,272 bytes maximum residency (13741 sample(s))
         887,768 bytes maximum slop
               9 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0     46734 colls, 46734 par   41.66s   20.87s     0.0004s    0.0058s
  Gen  1     13741 colls, 13740 par   23.18s   11.62s     0.0008s    0.0041s

  Parallel GC work balance: 60.58% (serial 0%, perfect 100%)

  TASKS: 4 (1 bound, 3 peak workers (3 total), using -N2)

  SPARKS: 200 (200 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time   34.99s  ( 17.60s elapsed)
  GC      time   64.85s  ( 32.49s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   99.84s  ( 50.08s elapsed)

  Alloc rate    1,732,048,869 bytes per MUT second

  Productivity  35.0% of total user, 69.9% of total elapsed

私のマシン上(-N2物理コアが2つしかないため)。型署名を使用するようにコードを変更するだけで、次のようになりますfloor q :: Int

./nstdmap +RTS -s -N2 > /dev/null
  52,105,495,488 bytes allocated in the heap
  29,957,007,208 bytes copied during GC
       2,440,568 bytes maximum residency (10481 sample(s))
         893,224 bytes maximum slop
               8 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0     36979 colls, 36979 par   32.96s   16.51s     0.0004s    0.0066s
  Gen  1     10481 colls, 10480 par   16.65s    8.34s     0.0008s    0.0018s

  Parallel GC work balance: 68.64% (serial 0%, perfect 100%)

  TASKS: 4 (1 bound, 3 peak workers (3 total), using -N2)

  SPARKS: 200 (200 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.01s  (  0.01s elapsed)
  MUT     time   29.78s  ( 14.94s elapsed)
  GC      time   49.61s  ( 24.85s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   79.40s  ( 39.80s elapsed)

  Alloc rate    1,749,864,775 bytes per MUT second

  Productivity  37.5% of total user, 74.8% of total elapsed

経過時間は約20%、MUT時間は13%削減されます。悪くない。floor最適化で得られるコードを見ると、その理由がわかります。

floorDoubleInt :: Double -> Int
floorDoubleInt (D# x) =
    case double2Int# x of
      n | x <## int2Double# n   -> I# (n -# 1#)
        | otherwise             -> I# n

floorDoubleInteger :: Double -> Integer
floorDoubleInteger (D# x) =
    case decodeDoubleInteger x of
      (# m, e #)
        | e <# 0#   ->
          case negateInt# e of
            s | s ># 52#    -> if m < 0 then (-1) else 0
              | otherwise   ->
                case TO64 m of
                  n -> FROM64 (n `uncheckedIShiftRA64#` s)
        | otherwise -> shiftLInteger m e

floor :: Double -> Intマシン変換を使用するだけですfloor :: Double -> Integerが、高価decodeDoubleIntegerでより多くのブランチが必要です。しかし、ここでと呼ばれる場合、関係するすべてのsは非負でfloorあることがわかっているため、マシン変換に直接マップされる、と同じです。代わりに、それを試してみましょう。Doublefloortruncatedouble2Int#floor

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time   29.29s  ( 14.70s elapsed)
  GC      time   49.17s  ( 24.62s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   78.45s  ( 39.32s elapsed)

非常に小さな削減​​です(予想されるように、これfmodは実際にはボトルネックではありません)。比較のために、Cに呼びかけます。

  INIT    time    0.01s  (  0.01s elapsed)
  MUT     time   31.46s  ( 15.78s elapsed)
  GC      time   54.05s  ( 27.06s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   85.52s  ( 42.85s elapsed)

は少し遅いです(当然のことながら、Cの呼び出しにかかる時間内にいくつかのprimopsを実行できます)。

しかし、それは大きな魚が泳ぐ場所ではありません。悪い点はm、軌道のすべての要素のみを選択すると、大きなサンクが発生し、多くの割り当てが発生し、時が来たときに評価するのに時間がかかることです。それでは、そのリークを排除し、軌道を厳密にしましょう。

{-# LANGUAGE BangPatterns #-}

trajectory :: (Point -> Point) -> Point -> [Point] 
trajectory map !initial@(!a,!b) = initial : (trajectory map $ map initial)

これにより、割り当てとGC時間が大幅に短縮され、その結果、MUT時間も短縮されます。

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time   21.83s  ( 10.95s elapsed)
  GC      time    0.72s  (  0.36s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   22.55s  ( 11.31s elapsed)

オリジナルfmodで、

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time   18.26s  (  9.18s elapsed)
  GC      time    0.58s  (  0.29s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   18.84s  (  9.47s elapsed)

を使用しfloor q :: Int、測定精度の範囲内で同じ時間truncate q :: Int(の割り当て値は少し低くなりtruncateます)。


問題はオーバーフローしたSPARKSの数にあるようです。これは、K = 8000の場合は0、K=16000の場合は7802です。これはおそらく同時実行性の悪さに反映されています

はい(私が知る限り、ここでのより正確な用語は並行性ではなく並列処理です)、スパークプールがあり、それがいっぱいになると、次のスレッドで評価される予定はありません。来ると、計算は親スレッドから並列処理なしで実行されます。この場合、これは、最初の並列フェーズの後、計算がシーケンシャルにフォールバックすることを意味します。

スパークプールのサイズは明らかに約8K(2 ^ 13)です。

上からCPU負荷を見ると(close to 100%)*(number of cores)、しばらくすると、CPU負荷がはるかに低い値に低下することがわかります(私にとっては、で約100%、-N2で約130%でした-N4)。

治療法は、火花が多すぎないようにし、各火花にさらに作業を行わせることです。迅速で汚い変更で

ensembleTrace orbitGen observable n m =
    withStrategy (parListChunk 25 rdeepseq) . map ((map observable . subTrace n m) . orbitGen)

-N2実質的にすべての実行と優れた生産性で200%に戻りました。

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time   57.42s  ( 29.02s elapsed)
  GC      time    5.34s  (  2.69s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   62.76s  ( 31.71s elapsed)

  Alloc rate    1,982,155,167 bytes per MUT second

  Productivity  91.5% of total user, 181.1% of total elapsed

そして、-N4それも問題ありません(壁掛け時計では少し速くても、すべてのスレッドが基本的に同じであり、物理コアが2つしかないため、それほど多くはありません)、

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time   99.17s  ( 26.31s elapsed)
  GC      time   16.18s  (  4.80s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time  115.36s  ( 31.12s elapsed)

  Alloc rate    1,147,619,609 bytes per MUT second

  Productivity  86.0% of total user, 318.7% of total elapsed

今からスパークプールはオーバーフローしません。

適切な修正は、スパークの数がプールサイズを超えないように、チャンクのサイズを軌道と使用可能なコアの数から計算されるパラメーターにすることです。

于 2012-11-09T01:37:15.530 に答える
2

いくつかの簡単なプロファイリングを行った後、私はこれらが連続犯罪者であることに気づきました:

ghc --make -O2 MainNonOpt.hs -threaded -prof -auto-all -caf-all -fforce-recomp
./MainNonOpt +RTS -N4 -p > /dev/null

>>>
COST CENTRE MODULE  %time %alloc
fmod        Main     46.3   33.3
standardMap Main     28.5    0.0
trajectory  Main     23.8   66.6

fmodについて驚くべきことは、それがほとんど数値関数であることを考慮して、fmodが行う割り当ての数が多いことです。したがって、次のステップは、fmodに注釈を付けて、問題がどこにあるかを確認することです。

fmod :: Double -> Double -> Double
fmod a b | a < 0     = {-# SCC "negbranch" #-} b - fmod (abs a) b 
         | otherwise = {-# SCC "posbranch" #-} if a < b then a 
                       else let q = {-# SCC "division" #-} a / b in {-# SCC "expression" #-} b * (q - {-# SCC "floor" #-} fromIntegral (floor q))

これは私たちに与えます:

ghc --make -O2 MainNonOpt.hs -threaded -prof -caf-all -fforce-recomp
./MainNonOpt +RTS -N4 -p > /dev/null

COST CENTRE MODULE  %time %alloc

MAIN        MAIN     61.5   70.0
posbranch   Main     16.6    0.0
floor       Main     14.9   30.0
expression  Main      4.5    0.0
negbranch   Main      1.9    0.0

したがって、floor問題の原因となるのはとのビットです。周りを見回したところ、プレリュードはいくつかのDouble RealFrac関数を適切に実装しておらず(ここを参照)、おそらくいくつかのボクシング/アンボクシングを引き起こしていることがわかりました。

したがって、リンクからのアドバイスに従うことで、fromIntegralへの呼び出しも不要にする変更バージョンのfloorを使用しました。

floor' :: Double -> Double
floor' x = c_floor x
{-# INLINE floor' #-} 

foreign import ccall unsafe "math.h floor" 
   c_floor :: Double -> Double 


fmod :: Double -> Double -> Double
fmod a b | a < 0     = {-# SCC "negbranch" #-} b - fmod (abs a) b
         | otherwise = {-# SCC "posbranch" #-} if a < b then a
                       else let q = {-# SCC "division" #-} a / b in {-# SCC "expression" #-} b * (q - ({-# SCC "floor" #-} floor' q))

編集:ダニエルフィッシャーが指摘するように、パフォーマンスを向上させるためにCコードをインライン化する必要はありません。類似のHaskell関数はすでに存在します。とにかく答えを残しておきます。

これは違いを生みます。私のマシンでは、k = l = 200、M = 5000の場合、最適化されていないバージョンと最適化されたバージョンの数値は次のとおりです。

最適化されていない:

real    0m20.635s
user    1m17.321s
sys     0m4.980s

最適化:

real    0m14.858s
user    0m55.271s
sys     0m3.815s

このtrajectory関数にも同様の問題がある可能性があり、上記で使用したようなプロファイリングを使用して問題を特定できます。

Haskellでのプロファイリングの優れた出発点は、RealWorldHaskellのこの章にあります。

于 2012-11-08T23:41:19.503 に答える