Jak zmniejszyć zużycie pamięci w aplikacji Haskell?
Jestem nowy w programowaniu funkcyjnym, a teraz uczyć się Haskell. Jako ćwiczenie postanowiłem wdrożyć metodę Eulera dla równania dyfuzji liniowej 1D. Podczas gdy poniższy kod działa poprawnie, nie jestem zadowolony z jego wydajności. Chodzi mi o zużycie pamięci. Uważam, że jest to związane z leniwą oceną, ale nie mogę dowiedzieć się, jak mogę zmniejszyć zużycie pamięci.
Idea algorytmu jest naprawdę prosta, aby było jasne w kategoriach imperatywnych: to przyjmuje 'tablicę' i do każdego wewnętrznego punktu dodaje wartość, która jest obliczana jako kombinacja wartości w samym punkcie i w jego sąsiadach. Punkty graniczne są szczególnymi przypadkami.
Więc to jest mój moduł Euler1D. hs:
module Euler1D
( stepEuler
, makeu0
) where
-- impose zero flux condition
zeroflux :: (Floating a) => a -> [a] -> [a]
zeroflux mu (boundary:inner:xs) = [boundary+mu*2*(inner-boundary)]
-- one step of integration
stepEuler :: (Floating a) => a -> [a] -> [a]
stepEuler mu u@(x:xs) = (applyBC . (diffused mu)) u
where
diffused mu (left:x:[]) = [] -- ignore outer points
diffused mu (left:x:right:xs) = -- integrate inner points
(x+mu*(left+right-2*x)) : diffused mu (x:right:xs)
applyBC inner = (lbc u') ++ inner ++ (rbc u') -- boundary conditions
where u' = [head u] ++ inner ++ [last u]
lbc = zeroflux mu -- left boundary
rbc = (zeroflux mu) . reverse -- right boundary
-- initial condition
makeu0 :: Int -> [Double]
makeu0 n = [ ((^2) . sin . (pi*) . xi) x | x <- [0..n]]
where xi x = fromIntegral x / fromIntegral n
I prosty główny.hs:
module Main where
import System ( getArgs )
import Euler1D
main = do
args <- getArgs
let n = read $ head args :: Int
let u0 = makeu0 n
let un = stepEuler 0.5 u0
putStrLn $ show $ sum un
Dla porównania napisałem również czystą implementację C.
Teraz, jeśli spróbuję uruchomić implementację Haskella dla wystarczająco dużego rozmiaru tablicy n
, mieć:
$ time ./eulerhs 200000
100000.00000000112
real 0m3.552s
user 0m3.304s
sys 0m0.128s
Dla porównania, Wersja C jest szybsza o prawie dwa rzędy wielkości:]}
$ time ./eulerc 200000
100000
real 0m0.088s
user 0m0.048s
sys 0m0.008s
EDIT: to porównanie nie jest naprawdę fair, ponieważ wersja Haskell jest skompilowane z profilowanymi flagami i C nie jest. Jeśli skompiluję oba programy z
[[18]}chciałbym również zauważyć, że pamięć przydział programu Haskell wydaje się rosnąć w linii z-O2
i obie bez profilowania flagi, mogę zwiększyćn
. W tym casetime ./eulerhs 1000000
zajmuje 0m2.236S, podczas gdytime ./eulerc 1000000
zajmuje tylko 0M0. 293s. Tak więc problem nadal pozostaje ze wszystkimi optymalizacje i bez profilowanie, to tylko przesunięcie.n
. To chyba OK.
Ale najgorsze są wymagania dotyczące pamięci. Moja wersja Haskell wymaga ponad 100MB (moje oszacowanie minimum w C to 4MB ). Myślę, że to może być źródłem problemu. Zgodnie z raportem profilowania program spędza 85% czasu w GC i
total time = 0.36 secs (18 ticks @ 20 ms)
total alloc = 116,835,180 bytes (excludes profiling overheads)
COST CENTRE MODULE %time %alloc
makeu0 Euler1D 61.1 34.9
stepEuler Euler1D 33.3 59.6
CAF:sum Main 5.6 5.5
Byłem / align = "center" bgcolor = "# e0ffe0 " / cesarz chin / / align = center / Zdecydowałem, że jest to spowodowane jego leniwą oceną (jeśli jego kawałki pozostaną w pamięci do końca stepEuler
).
Próbowałem tej zmiany w Main.hs
:
let un = u0 `seq` stepEuler 0.5 u0
Ale nie zauważyłem żadnej różnicy. Nie mam pojęcia, jak zmniejszyć zużycie pamięci w stepEuler
. Moje pytania to:
- czy w Haskell istnieje sposób na tworzenie list / składanie list ściśle? W tym przypadku nie ma korzyści, aby utrzymać go leniwy.
- Jak mogę zmniejszyć ogólne zużycie pamięci w tym przypadku? Przypuszczam, że muszę zrobić coś ścisłego, ale nie wiem, co. Innymi słowy, jeśli muszę umieścić jakieś
seq
S i grzywkę, gdzie i dlaczego? - i wreszcie, najważniejsze, jaka jest najlepsza strategia identyfikacji tak kosztownych konstrukcji?
Czytałem rozdział na temat profilowania i optymalizacji wrealnym świecie Haskell , ale pozostaje niejasne, jak dokładnie mogę zdecydować, co powinno być surowe, a co nie.
Proszę wybacz mi taki długi post.
EDIT2: zgodnie z sugestią A. Rex w komentarzach, próbowałem uruchomić oba programy w valgrind. I to jest to, co Obserwowałem. Dla programu Haskell (
n
=200000) znaleziono:Malloc / free: 33 allocs, 30 frees, 84,109 bajtów przydzielonych. ... sprawdzono 55 712 980 bajtów.
I dla programu C (po małej poprawce):
Malloc / free: 2 allocs, 2 frees, 3,200,000 bajtów przydzielonych.
Wygląda więc na to, że podczas gdy Haskell przydzielanie znacznie mniejszych bloków pamięci, robi to często, a ze względu na opóźnienie w wywóz śmieci, gromadzą się i pozostaną w pamięci. Więc mam kolejne pytanie:
- czy można uniknąć wielu małe przydziały w Haskell? Zasadniczo, aby zadeklarować, że muszę przetwarzaj całą strukturę danych a nie tylko jego fragmenty na żądanie.
7 answers
Listy nie są najlepszą strukturą danych dla tego typu kodu (z dużą ilością ( ++ ) i (last)). Tracisz dużo czasu na konstruowanie i dekonstruowanie list. Użyłbym danych.Sekwencji lub tablic, jak w wersjach C.
Nie ma szans, aby kawałki makeu0 były zbierane jako śmieci, ponieważ musisz zachować wszystkie z nich (dokładnie wszystkie wyniki "rozproszone") aż do końca obliczeń, aby móc wykonać "odwrotne" w applyBC. Co jest bardzo droga rzecz, biorąc pod uwagę, że potrzebujesz tylko dwóch pozycji z ogona listy dla swojego "zeroflux".
Oto szybki hack Twojego kodu, który stara się osiągnąć lepszą fuzję list i mniej list (de)konstruuje:
module Euler1D
( stepEuler
) where
-- impose zero flux condition
zeroflux mu (boundary:inner:xs) = boundary+mu*2*(inner-boundary)
-- one step of integration
stepEuler mu n = (applyBC . (diffused mu)) $ makeu0 n
where
diffused mu (left:x:[]) = [] -- ignore outer points
diffused mu (left:x:right:xs) = -- integrate inner points
let y = (x+mu*(left+right-2*x))
in y `seq` y : diffused mu (x:right:xs)
applyBC inner = lbc + sum inner + rbc -- boundary conditions
where
lbc = zeroflux mu ((f 0 n):inner) -- left boundary
rbc = zeroflux mu ((f n n):(take 2 $ reverse inner)) -- right boundary
-- initial condition
makeu0 n = [ f x n | x <- [0..n]]
f x n = ((^2) . sin . (pi*) . xi) x
where xi x = fromIntegral x / fromIntegral n
Dla 200000 punktów, kończy się w 0.8 sekundy vs 3.8 sekundy dla początkowej wersji
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-29 03:00:38
W moim 32-bitowym systemie x86, Twój program używa tylko około 40 MB pamięci.
Czy może pomyliłeś linię" total alloc = 116,835,180 bajtów " z wyjściowego profilu z ilością pamięci faktycznie używanej przez program w danym momencie? Całkowita wartość alloc to ilość pamięci przydzielonej przez cały program; wiele z tego jest zwalnianych przez garbage collector w trakcie pracy. Można się spodziewać, że liczba ta będzie bardzo duża w programie Haskell; mam programy, które przydzielają wiele terrabajtów pamięci w ciągu całego ich biegu, chociaż w rzeczywistości mają maksymalny rozmiar pamięci wirtualnej około stu megabajtów.
Nie martwiłbym się zbytnio o Duże całkowite alokacje w trakcie uruchamiania programu; taka jest natura czystego języka, a środowisko uruchomieniowe GHC ma bardzo dobry garbage collector, który pomaga to zrekompensować.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-06 08:44:15
Bardziej ogólnie, możesz dowiedzieć się, gdzie idzie twoja pamięć za pomocą narzędzi do profilowania stosu GHC. z mojego doświadczenia wynika, że niekoniecznie powiedzą ci, dlaczego Twoje dane są wyciekane, ale mogą przynajmniej zawęzić potencjalne przyczyny.
Możesz również znaleźć iluminację tego doskonały post na blogu autorstwa Dona Stewarta o zrozumieniu surowości, jak współdziała ona ze śmieciarstwem oraz jak diagnozować i naprawiać problemy.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-11 16:21:07
Czy wymuszanie "nie-lenistwa" za pomocą $! pomóc? zgodnie z Ta odpowiedź .
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:32:20
Na prośbę Harleqina: próbowałeś ustawić flagi optymalizacji? Na przykład, w przypadku ghc, możesz użyć add "- O2", tak jak w przypadku gcc. (Chociaż nie jestem pewien, jakie poziomy optymalizacji istnieją w ghc; strona man nie mówi dokładnie ...)
W moim poprzednim doświadczeniu ustawienie tej flagi zrobiłoogromną różnicę. Z tego co wiem, runhugs
i unoptimized ghc
używają najbardziej podstawowej, oczywistej implementacji Haskell; niestety, czasami nie jest to zbyt wydajny.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-20 06:29:39
Użyj również przełącznika -fvia-C
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-20 13:27:57
Jedną rzeczą, która rzuciła mi się w oczy jest to, że wyjście Haskella jest float, podczas gdy wyjście C wydaje się być liczbą całkowitą. Nie mam jeszcze do czynienia z kodem Haskella, ale czy jest może jakieś miejsce, gdzie masz arytmetykę zmiennoprzecinkową w Haskell podczas gdy C używa liczb całkowitych?
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-20 09:40:45