Подтвердить что ты не робот

Как уменьшить использование памяти в приложении Haskell?

Я новичок в функциональном программировании, а теперь изучаю 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 [email protected](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 и оба без профилирования flags, я могу увеличить 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 и bangs, где и почему?
  • И, наконец, самое главное, что является лучшей стратегией для определения таких дорогостоящих конструкций?

Я прочитал главу о профилировании и оптимизации в Real World Haskell, но остается неясным, как именно я могу решить, что должно быть строгим и что нет.

Пожалуйста, простите мне такой длинный пост.

EDIT2: Как было предложено А. Рексом в комментариях, я попытался запустить оба программы в valgrind. И это то, что Я заметил. Для программы Haskell (n= 200000) он нашел:

malloc/free: 33 allocs, 30 frees, выделено 84109 байт.   ...   проверено 55 712 980 байт.

И для программы C (после небольшого исправления):

malloc/free: 2 allocs, 2 frees, выделено 3 200 000 байт.

Итак, кажется, что в то время как Haskell выделяет значительно меньшие блоки памяти, он делает это часто, и из-за задержки в сбор мусора, они накапливаются и остаются в памяти. Так что я другой вопрос:

  • Можно ли избежать многих небольшие ассигнования в Haskell? В принципе, заявить, что мне нужно обрабатывать всю структуру данных а не только его фрагменты на спрос.
4b9b3361

Ответ 1

  • Списки не являются лучшей структурой данных для этого типа кода (с большим количеством (++) и (последним)). Вы теряете много времени, составляя и деконструируете списки. Я бы использовал Data.Sequence или массивы, как в версиях C.

  • Нет шансов на сбор мусора, поскольку вам нужно сохранить все (ну, все результаты "диффузного", если быть точным) до конца до конца вычислений, чтобы иметь возможность "обратного" в примененииBC. Это очень дорогая вещь, учитывая, что вам нужны только два элемента из хвоста списка для вашего "нуля".

Здесь вы можете быстро взломать код, который пытается добиться лучшего слияния списков и делает меньше list (de):

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

За 200000 очков он заканчивается через 0,8 секунды против 3,8 секунды для начальной версии

Ответ 2

В моей 32-разрядной системе x86 ваша программа использует только около 40 МБ памяти.

Возможно, вы сбиваете с толку строку "total alloc = 116,835,180 bytes" из вашего профилирующего вывода, сколько памяти фактически используется программой в любой момент? Общее значение - сколько памяти было выделено за весь прогон программы; большая часть этого освобождается сборщиком мусора, когда вы идете вперед. Вы можете ожидать, что число будет очень большим в программе Haskell; У меня есть программы, которые выделяют много терабайтов памяти в течение всего их прогона, хотя на самом деле они имеют максимальную виртуальную память размером в сто мегабайт или около того.

Я бы не стал слишком беспокоиться о больших общих ассигнованиях в ходе прогона программы; что природа чистого языка и время выполнения GHC имеют очень хороший сборщик мусора, чтобы помочь компенсировать это.

Ответ 3

В более общем плане вы можете узнать, где находится ваша память, используя инструменты для профилирования кучи GHC. По моему опыту, они не обязательно скажите, почему ваши данные просачиваются, но могут по крайней мере сузить потенциальные причины.

Вы также можете найти иллюстрацию этого отличного сообщения в блоге Дона Стюарта о понимании строгости, о том, как он взаимодействует с сборкой мусора, и как диагностировать и устранить проблемы.

Ответ 4

Заставляет "un-laziness" использовать $! Помогите? как этот ответ.

Ответ 5

В соответствии с запросом Harleqin: вы пытались установить флаги оптимизации? Например, с помощью ghc вы можете использовать "-O2" так же, как и с gcc. (Хотя я не уверен, какие уровни оптимизации существуют в ghc, man-страница точно не говорит...)

В моем прошлом опыте установка этого флага сделала огромную разницу. Насколько я могу судить, runhugs и неоптимизированный ghc используют самую основную, очевидную реализацию Haskell; к сожалению, это иногда не очень эффективно.

Но это просто догадка. Как я сказал в своем комментарии, я надеюсь, что кто-то хорошо ответит на ваш вопрос. У меня часто возникают проблемы с анализом и исправлением использования памяти Haskell.

Ответ 6

Используйте также переключатель -fvia-C.

Ответ 7

Одна вещь, которая подскочила на мой взгляд, заключается в том, что вывод Haskell является float, а вывод C - целым. Я еще не справился с кодом Haskell, но возможно ли место, где у вас есть арифметика с плавающей запятой в Haskell, а C использует целые числа?