Ich wollte eine effiziente Implementierung der Floyd-Warshall alle Paare kürzester Weg Algorithmus in Haskell mit Vector
s, um hoffentlich gute Leistung zu bekommen.Performance von Floyd-Warshall in Haskell - Fixing ein Leck im Raum
Die Implementierung ist recht einfach, aber anstatt ein 3-dimensionales | V | × | V | × | V | zu verwenden Matrix wird ein 2-dimensionaler Vektor verwendet, da wir immer nur den vorherigen k
Wert lesen.
Somit ist der Algorithmus wirklich nur eine Reihe von Schritten, wo ein 2D-Vektor übergeben wird und ein neuer 2D-Vektor generiert wird. Der letzte 2D-Vektor enthält die kürzesten Wege zwischen allen Knoten (i, j).
Meine Intuition sagte mir, dass es wichtig wäre, um sicherzustellen, dass der vorherige 2D-Vektor vor jedem Schritt bewertet wurde, so habe ich BangPatterns
auf das prev
Argument die fw
Funktion und die strengen foldl'
:
{-# 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
Wenn jedoch dieses Programm mit einem 1000-Node-Graphen mit 47978 Kanten ausgeführt wird, sieht die Sache gar nicht gut aus. Die Speicherauslastung ist sehr hoch und das Programm dauert viel zu lange. Das Programm wurde mit ghc -O2
kompiliert.
umgebaut ich das Programm für die Profilierung und begrenzt die Anzahl der Iterationen bis 50:
:results = foldl' (fw g v) initial [1..50]
ich das Programm mit +RTS -p -hc
und +RTS -p -hd
dann lief
Das ist ... interessant, aber ich denke, es zeigt, dass es acc ist Umhüllen von Tonnen von Thunks. Nicht gut.
Ok, also nach ein paar Schüsse in der Dunkelheit, fügte ich ein deepseq
in fw
um sicherzustellen, dass prev
wirklich ist evaluted:
let d = prev `deepseq` distance g prev i j k
Nun liegen die Dinge besser aussehen, und ich kann das Programm tatsächlich laufen zur Vervollständigung mit konstanter Speichernutzung. Es ist offensichtlich, dass der Knall auf dem Argument prev
nicht genug war.
Zum Vergleich mit den vorherigen Graphen, hier ist der Speicherverbrauch für 50 Iterationen nach der deepseq
Zugabe:
Ok, so sind die Dinge besser, aber ich habe noch einige Fragen:
- Ist das die richtige Lösung für dieses Raumleck? Ich habe das Gefühl, dass das Einfügen eines
deepseq
ein bisschen hässlich ist? - Ist meine Verwendung von
Vector
hier idiomatisch/korrekt? Ich baue für jede Iteration einen komplett neuen Vektor und hoffe, dass der Garbage Collector die altenVector
s löschen wird. - Gibt es noch andere Dinge, die ich tun könnte, um das mit diesem Ansatz schneller zu machen?
Für Referenzen, hier ist graph.txt
: http://sebsauvage.net/paste/?45147f7caf8c5f29#7tiCiPovPHWRm1XNvrSb/zNl3ujF3xB3yehrxhEdVWw=
Hier ist 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')
eine Nebenbemerkung: 'jede nicht $ Karte (! \ I -> a i i> = 0) [0 .. (V.length a-1)]' 'ist irgendein (\ i -> a! i! i <0) [0 .. (V.länge a-1)] '. –
haben Sie versucht, Ihre 'foldl'- und' forM_'-Berechnungen als explizite Schleifen mit veränderbaren Vektoren neu zu schreiben? (wie zB [in 'test0' hier] (http://codereview.stackexchange.com/a/24968/9064), allerdings mit Arrays, nicht Vektoren. und [hier mit Schleifen anstelle von üblichen' forM'] (http://stackoverflow.com/a/15026238/849891)) –
@WillNess: Nein, das einzige, was ich war versucht, 'mit einer Schwanz-rekursive Funktion mit einem strengen Speicher zu ersetzen foldl'', aber das schien nicht zu wirken. Es ist etwas entmutigend zu sehen, dass beide Beispiele, auf die Sie verlinken, mit 'unsicheren * Funktionen übersät sind - ich hatte wirklich gehofft, dass es möglich ist, eine vernünftige Leistung zu erreichen, ohne darauf zurückzugreifen. :-) – beta