Я новичок в функциональном программировании и теперь изучаю Haskell. В качестве упражнения я решил реализовать явный метод Эйлера для одномерного линейного уравнения диффузии. Хотя приведенный ниже код работает правильно, я не доволен его производительностью. На самом деле меня беспокоит потребление памяти. Я считаю, что это связано с ленивой оценкой, но не могу понять, как уменьшить использование памяти.
Идея алгоритма действительно проста, чтобы прояснить ее в императивных терминах: он принимает `массив 'и к каждой внутренней точке добавляет значение, которое вычисляется как комбинация значений в самой точке и в ее соседи. Граничные точки - это особые случаи.
Итак, это мой модуль 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
И простой Main.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
Для сравнения я также написал чистую реализацию C.
Теперь, если я попытаюсь запустить реализацию Haskell для достаточно большого размера массива n
, у меня будет:
$ time ./eulerhs 200000
100000.00000000112
real 0m3.552s
user 0m3.304s
sys 0m0.128s
Для сравнения, версия C быстрее почти на два порядка:
$ time ./eulerc 200000
100000
real 0m0.088s
user 0m0.048s
sys 0m0.008s
EDIT: это сравнение не совсем справедливо, потому что версия Haskell скомпилирована с флагами профилирования, а C - нет. Если я скомпилирую обе программы с
-O2
и обе без флагов профилирования, я могу увеличитьn
. В этом случаеtime ./eulerhs 1000000
занимает 0m2,236s, аtime ./eulerc 1000000
- только 0m0,293s. Таким образом, проблема остается со всеми оптимизациями и без профилирования, она только компенсируется.Я также хотел бы отметить, что распределение памяти программы Haskell, похоже, растет линейно с
n
. Наверное, это нормально.
Но хуже всего - требования к памяти. Моя версия Haskell требует более 100 МБ (моя оценка минимума в C составляет 4 МБ). Думаю, это может быть источником проблемы. Согласно отчету о профилировании программа проводит 85% времени в GC, и
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
Я был удивлен, увидев, что makeu0
так дорого. Я решил, что это связано с его ленивой оценкой (если его преобразователи остаются в памяти до конца stepEuler
).
Я пробовал это изменение в Main.hs
:
let un = u0 `seq` stepEuler 0.5 u0
но разницы не заметил. Понятия не имею, как уменьшить использование памяти в stepEuler
. Итак, мои вопросы:
- Есть ли способ в Haskell строить списки / выполнять их строгое понимание? В этом случае нет смысла ленить его.
- Как в этом случае уменьшить общее использование памяти? Полагаю, надо сделать что-то строгое, но не вижу что. Другими словами, если мне нужно нанести
seq
челку, где и зачем? - И, наконец, самое главное, как лучше всего определять такие дорогостоящие конструкции?
Я прочитал главу о профилировании и оптимизации в Real World Haskell, но остается неясным, как именно я могу решить, что должно быть строгим, а что нет.
Прошу простить меня за такой длинный пост.
EDIT2: как было предложено А. Рексом в комментариях, я попытался запустить обе программы в valgrind. И это то, что я заметил. Для программы Haskell (
n
= 200000) обнаружено:malloc / free: 33 аллока, 30 освобождений, выделено 84 109 байт. ... проверил 55 712 980 байт.
И для программы C (после небольшого исправления):
malloc / free: 2 аллока, 2 освобождения, выделено 3 200 000 байт.
Итак, похоже, что, хотя Haskell выделяет гораздо меньшие блоки памяти, он делает это часто, и из-за задержки сборки мусора они накапливаются и остаются в памяти. Итак, у меня есть еще один вопрос:
- Можно ли избежать большого количества небольших распределений в Haskell? По сути, заявить, что мне нужно обрабатывать всю структуру данных, а не только ее фрагменты по запросу.
n
.time ./eulerhs 1000000
выполняется за 2,236 с, аtime ./eulerc 1000000
- за 0,293 с. Отредактирую вопрос. Спасибо за комментарии! - person sastanin   schedule 20.01.2009