8

Vectorうまくいけば良いパフォーマンスが得られるように、s を使用して Haskell で Floyd-Warshall の全ペア最短経路アルゴリズムの効率的な実装を書きたかったのです。

実装は非常に簡単ですが、代わりに 3 次元の |V|×|V|×|V| を使用します。前の値のみを読み取るため、2 次元ベクトルが使用されkます。

したがって、このアルゴリズムは実際には、2D ベクトルが渡されて新しい 2D ベクトルが生成される一連のステップにすぎません。最終的な 2D ベクトルには、すべてのノード (i,j) 間の最短パスが含まれます。

私の直感では、各ステップの前に前の 2D ベクトルが評価されていることを確認することが重要であることがわかったので、関数の引数と strictを使用BangPatternsしました。prevfwfoldl'

{-# Language BangPatterns #-}

import           Control.DeepSeq
import           Control.Monad       (forM_)
import           Data.List           (foldl')
import qualified Data.Map.Strict     as M
import           Data.Vector         (Vector, (!), (//))
import qualified Data.Vector         as V
import qualified Data.Vector.Mutable as V hiding (length, replicate, take)

type Graph = Vector (M.Map Int Double)
type TwoDVector = Vector (Vector Double)

infinity :: Double
infinity = 1/0

-- calculate shortest path between all pairs in the given graph, if there are
-- negative cycles, return Nothing
allPairsShortestPaths :: Graph -> Int -> Maybe TwoDVector
allPairsShortestPaths g v =
  let initial = fw g v V.empty 0
      results = foldl' (fw g v) initial [1..v]
  in if negCycle results
        then Nothing
        else Just results
  where -- check for negative elements along the diagonal
        negCycle a = any not $ map (\i -> a ! i ! i >= 0) [0..(V.length a-1)]

-- one step of the Floyd-Warshall algorithm
fw :: Graph -> Int -> TwoDVector -> Int -> TwoDVector
fw g v !prev k = V.create $ do                                           -- ← bang
  curr <- V.new v
  forM_ [0..(v-1)] $ \i ->
    V.write curr i $ V.create $ do
      ivec <- V.new v
      forM_ [0..(v-1)] $ \j -> do
        let d = distance g prev i j k
        V.write ivec j d
      return ivec
  return curr

distance :: Graph -> TwoDVector -> Int -> Int -> Int -> Double
distance g _ i j 0 -- base case; 0 if same vertex, edge weight if neighbours
  | i == j    = 0.0
  | otherwise = M.findWithDefault infinity j (g ! i)
distance _ a i j k = let c1 = a ! i ! j
                        c2 = (a ! i ! (k-1))+(a ! (k-1) ! j)
                        in min c1 c2

ただし、47978 個のエッジを持つ 1000 ノードのグラフでこのプログラムを実行すると、まったくうまくいきません。メモリ使用量が非常に高く、プログラムの実行に時間がかかりすぎます。プログラムは でコンパイルされましたghc -O2

プロファイリング用にプログラムを再構築し、反復回数を 50 に制限しました。

 results = foldl' (fw g v) initial [1..50]

+RTS -p -hc次に、 andを使用してプログラムを実行しました+RTS -p -hd

これは... 興味深いですが、サンクが大量に蓄積されていることを示していると思います。良くない。

わかりましたので、暗闇で数回撮影した後、実際に評価されることを確認するために を追加deepseqしました。fwprev

let d = prev `deepseq` distance g prev i j k

これで見た目が良くなり、一定のメモリ使用量で実際にプログラムを最後まで実行できます。議論の強打prevが十分でなかったことは明らかです。

前のグラフとの比較のために、以下に を追加した後の 50 回の反復のメモリ使用量を示しますdeepseq

さて、状況は良くなりましたが、まだいくつか質問があります。

  1. これは、このスペース リークの正しい解決策ですか? deepseqa を挿入するのが少し醜いと思うのは間違っていますか?
  2. ここでの s の使用法はVector慣用的/正しいですか? 反復ごとにまったく新しいベクトルを構築しており、ガベージ コレクターが古いVectors を削除することを期待しています。
  3. このアプローチでこれをより速く実行するために他にできることはありますか?

参考までに、httpgraph.txt : //sebsauvage.net/paste/?45147f7caf8c5f29#7tiCiPovPHWRm1XNvrSb/zNl3ujF3xB3yehrxhEdVWw= をご覧ください。

ここにあるmain

main = do
  ls <- fmap lines $ readFile "graph.txt"
  let numVerts = head . map read . words . head $ ls
  let edges = map (map read . words) (tail ls)
  let g = V.create $ do
        g' <- V.new numVerts
        forM_ [0..(numVerts-1)] (\idx -> V.write g' idx M.empty)
        forM_ edges $ \[f,t,w] -> do
          -- subtract one from vertex IDs so we can index directly
          curr <- V.read g' (f-1)
          V.write g' (f-1) $ M.insert (t-1) (fromIntegral w) curr
        return g'
  let a = allPairsShortestPaths g numVerts
  case a of
    Nothing -> putStrLn "Negative cycle detected."
    Just a' -> do
      putStrLn  $ "The shortest, shortest path has length "
              ++ show ((V.minimum . V.map V.minimum) a')
4

1 に答える 1

5

まず、いくつかの一般的なコードのクリーンアップ:

fw関数では、変更可能なベクトルを明示的に割り当てて埋めます。ただし、この正確な目的のために事前に作成された関数、つまり がありgenerateます。fwしたがって、次のように書き換えることができます

V.generate v (\i -> V.generate v (\j -> distance g prev i j k))

replicate同様に、グラフ生成コードはおよびに置き換えることができますaccum

let parsedEdges = map (\[f,t,w] -> (f - 1, (t - 1, fromIntegral w))) edges
let g = V.accum (flip (uncurry M.insert)) (V.replicate numVerts M.empty) parsedEdges

これにより、パフォーマンスを失うことなく、ミューテーションの必要性が完全になくなることに注意してください。

さて、実際の質問に:

  1. 私の経験でdeepseqは、非常に便利ですが、このようなスペース リークの迅速な修正にすぎません。根本的な問題は、結果を生成した後に結果を強制する必要があるということではありません。代わりに、の使用は、deepseqそもそも構造をより厳密に構築する必要があったことを意味します。実際、次のようにベクター作成コードに bang パターンを追加すると:

    let !d = distance g prev i j k
    

    その後、問題はなしで修正されdeepseqます。これはコードでは機能しないことに注意してください。これはgenerate、何らかの理由で (これについて機能要求を作成する可能性があります)、vectorボックス化されたベクトルに対して厳密な機能を提供しないためです。ただし、厳密な質問 3 への回答でボックス化されていないベクトルにたどり着くと、どちらのアプローチも厳密性注釈なしで機能します。

  2. 私の知る限り、新しいベクトルを繰り返し生成するパターンは慣用的です。慣用的ではない唯一のことは、可変性の使用です-厳密に必要な場合を除いて、可変ベクトルは一般的に推奨されません。

  3. いくつかの作業があります。

    • 最も簡単に言えば、 に置き換えることができMap IntますIntMap。これは実際には関数のスロー ポイントではないため、あまり問題にはなりませんが、IntMap負荷の高いワークロードでははるかに高速になる可能性があります。

    • ボックス化されていないベクトルの使用に切り替えることができます。外側のベクトルはボックス化されたままにする必要がありますが、ベクトルのベクトルはボックス化を解除できないため、内側のベクトルはボックス化できます。これにより、厳密性の問題も解決されます。ボックス化されていないベクトルは要素が厳密であるため、スペースリークは発生しません。私のマシンでは、これによりパフォーマンスが 4.1 秒から 1.3 秒に向上することに注意してください。したがって、ボックス化解除は非常に役立ちます。

    • ベクトルを 1 つのベクトルにフラット化し、乗算と除算を使用して 2 次元インデックスと 1 次元インデックスを切り替えることができます。これはお勧めしません。少し複雑で、非常に見苦しく、分割によって実際にマシンのコードが遅くなるからです。

    • 使用できますrepa。これには、コードを自動的に並列化できるという大きな利点があります。repa配列を平坦化し、適切に埋めるために必要な分割を明らかに適切に削除しないため (ネストされたループを使用することは可能ですが、単一のループと分割を使用すると思います)、同じパフォーマンスのペナルティがあることに注意してください。上で述べたように、実行時間が 1.3 秒から 1.8 秒になりました。ただし、並列処理を有効にしてマルチコア マシンを使用すると、いくつかの利点が見え始めます。残念ながら、現在のテスト ケースは小さすぎてあまりメリットが見られません。そのため、私の 6 コア マシンでは、1.2 秒まで低下することがわかります。[1..v]代わりにサイズを元に戻すと[1..50]、並列処理により、32 秒から 13 秒に短縮されます。おそらく、このプログラムに大きな入力を与えると、より多くの利点が得られる可能性があります。

      興味があれば、ここに私のrepaバージョンを投稿しました。

    • 編集: を使用し-fllvmます。を使用してコンピューターでテストするrepaと、並列処理なしで 14.7 秒になりました。これは、並列処理なしと並列処理ありの場合とほぼ同じ-fllvmです。一般に、LLVM はこのような配列ベースのコードを非常にうまく処理できます。

于 2014-01-28T21:34:58.763 に答える