Chciałem napisać wydajną implementację algorytmu najkrótszej ścieżki Floyd-Warshall dla wszystkich par w Haskell za pomocą Vector
s, aby uzyskać dobrą wydajność.Wykonanie Floyd-Warshall w Haskell - Naprawianie wycieku przestrzeni
Implementacja jest dość prosta, ale zamiast trójwymiarowego | V | × | V | × | V | matrix, używany jest wektor dwuwymiarowy, ponieważ tylko czytamy poprzednią wartość k
.
Tak więc algorytm jest tak naprawdę serią kroków, w których wektor 2D jest przekazywany i generowany jest nowy wektor 2D. Ostateczny wektor 2D zawiera najkrótsze ścieżki między wszystkimi węzłami (i, j).
Moja intuicja podpowiadała mi, że ważne byłoby, aby upewnić się, że poprzedni 2D vector oceniano przed każdym kroku, więc użyłem BangPatterns
na prev
argument funkcji fw
i ścisłym 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
Jednak podczas uruchamiania tego programu z grafem o 1000 węzłach z 47978 krawędziami, rzeczy nie wyglądają dobrze. Wykorzystanie pamięci jest bardzo wysokie, a uruchomienie programu trwa zbyt długo. Program został skompilowany z ghc -O2
.
I przebudowany program do profilowania i ogranicza liczbę powtórzeń do 50:
results = foldl' (fw g v) initial [1..50]
Potem prowadził program z +RTS -p -hc
i +RTS -p -hd
:
To jest ... interesujące, ale myślę, że to pokazuje, że jest zgodne umiłowanie ton uderzeń. Niedobrze.
Ok, więc po kilku ujęć w ciemności, dodałem deepseq
w fw
aby upewnić prev
naprawdę jest evaluted:
let d = prev `deepseq` distance g prev i j k
Teraz wszystko wygląda lepiej, a ja mogę faktycznie uruchomić program do zakończenia ze stałym użyciem pamięci. To oczywiste, że huk na argumencie prev
nie wystarczył.
Dla porównania z poprzednich wykresów, tutaj jest wykorzystanie pamięci przez 50 powtórzeń po dodaniu deepseq
:
Ok, więc rzeczy są lepsze, ale nadal mam pewne pytania:
- Czy to właściwe rozwiązanie dla tego wycieku przestrzeni kosmicznej? Nie mam racji, czując, że wstawianie
deepseq
jest trochę brzydkie? - Czy korzystanie z
Vector
s tutaj idiomatyczne/poprawne? Buduję zupełnie nowy wektor dla każdej iteracji i mam nadzieję, że śmieciarz usunie stare s.Vector
. - Czy są inne rzeczy, które mogę zrobić, aby przyspieszyć ten proces?
Dla odniesienia, tutaj jest graph.txt
: http://sebsauvage.net/paste/?45147f7caf8c5f29#7tiCiPovPHWRm1XNvrSb/zNl3ujF3xB3yehrxhEdVWw=
Oto 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')
uwaga strona: 'Nie każdy $ map (! \ I -> a ja i> = 0) [0 .. (V.length a-1)]' 'każdy jest po prostu (\ i -> a! i! i <0) [0 .. (V.length a-1)] '. –
próbowałeś przepisać swoje obliczenia 'foldl'' i' forM_' jako wyraźne pętle używające zmiennych wektorów? (jak to zrobiono np. [w 'teście0' tutaj] (http://codereview.stackexchange.com/a/24968/9064), chociaż z tablicami, nie wektorami. i [tutaj z pętlami zamiast zwykłego' forM'] (http://stackoverflow.com/a/15026238/849891)) –
@WillNess: Nie, jedyną rzeczą, której próbowałem, było zastąpienie 'foldl'' funkcją rekursywnego ogona ze ścisłym akumulatorem, ale nie wydawało się to mieć efekt. To trochę przygnębiające, gdy widzimy, że oba przykłady, do których się łączysz, są zaśmiecone funkcjami "niebezpiecznymi *" - naprawdę miałem nadzieję, że udało się osiągnąć rozsądną wydajność bez uciekania się do tego. :-) – beta