34

私は関数型プログラミングの初心者で、Haskell を学んでいます。演習として、1D 線形拡散方程式に陽的オイラー法を実装することにしました。以下のコードは正しく動作しますが、そのパフォーマンスには満足できません。実際、私はメモリ消費に関心があります。遅延評価に関連していると思いますが、メモリ使用量を減らす方法がわかりません。

アルゴリズムの考え方は非常にシンプルで、命令的な用語で明確にします: 「配列」を取り、すべての内部ポイントに値を追加します。これは、ポイント自体とそのポイントの値の組み合わせとして計算されます。隣人。境界点は特殊なケースです。

これが私の 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 実装も作成しました。

ここで、十分に大きなサイズの array に対して Haskell 実装を実行しようとすると、次のnようになります。

$ time ./eulerhs 200000
100000.00000000112

real    0m3.552s
user    0m3.304s
sys     0m0.128s

比較のために、C バージョンはほぼ 2 桁高速です。

$ time ./eulerc 200000
100000

real    0m0.088s
user    0m0.048s
sys     0m0.008s

編集: Haskell バージョンはプロファイリング フラグを使用してコンパイルされており、C はそうではないため、この比較は実際には公平ではありません。-O2プロファイリング フラグを使用して、または使用せずに両方のプログラムをコンパイルすると、 を増やすことができnます。この場合time ./eulerhs 1000000、0m2.236 秒かかりますが、time ./eulerc 10000000m0.293 秒しかかかりません。そのため、すべての最適化を行っても問題は残り、プロファイリングを行わないと、相殺されるだけです。

また、Haskell プログラムのメモリ割り当ては、n. これで多分OKです。

しかし、最悪なのはメモリ要件です。私の Haskell バージョンでは100MB 以上が必要です (C での最低限の見積もりは4MBです)。これが問題の原因かもしれないと思います。プロファイリング レポートによると、プログラムは GC で 85% の時間を費やしており、

        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。だから、私の質問は次のとおりです。

  1. Haskell でリストを作成する方法/リスト内包表記を厳密に行う方法はありますか? この場合、怠惰にしておくメリットはありません。
  2. この場合、全体的なメモリ使用量を減らすにはどうすればよいですか? 私は何かを厳密にしなければならないと思いますが、何がわからないのですか。言い換えれば、seqs と bangs をいくつか入れなければならない場合、どこに、なぜ?
  3. そして最後に、最も重要なのは、そのようなコストのかかる構造を特定するための最善の戦略は何ですか?

Real World Haskellのプロファイリングと最適化に関する章を読みましたが、厳密であるべきものとそうでないものをどのように正確に決定できるかは不明のままです。

このような長い投稿をお許しください。

EDIT2:コメントでA. Rexが示唆したように、valgrindで両方のプログラムを実行してみました。そして、これが私が観察したことです。Haskell プログラム ( n=200000) の場合:

malloc/free: 33 個の割り当て、30 個の解放、84,109 バイトが割り当てられました。... 55,712,980 バイトを確認しました。

そしてCプログラムの場合(小さな修正後):

malloc/free: 2 つの割り当て、2 つの解放、3,200,000 バイトの割り当て。

したがって、Haskell ははるかに小さいメモリ ブロックを割り当てますが、頻繁に割り当て、ガベージ コレクションの遅延によりメモリに蓄積されたままになるようです。それで、私は別の質問があります:

  • Haskellで多くの小さな割り当てを避けることは可能ですか? 基本的には、必要に応じてデータ構造のフラグメントだけでなく、データ構造全体を処理する必要があることを宣言します。
4

7 に答える 7

20
  1. リストは、このタイプのコード((++)と(最後の)がたくさんある)に最適なデータ構造ではありません。リストの作成と分解に多くの時間を費やします。Cバージョンのように、Data.Sequenceまたは配列を使用します。

  2. makeu0のサンクがガベージコレクションされる可能性はありません。これは、計算が終了するまで、それらすべて(正確には、「diffuse」の結果すべて)を保持する必要があるためです。 applyBCで「リバース」を実行できます。「zeroflux」のリストの末尾から2つのアイテムしか必要ないことを考えると、これは非常に高価なことです。

これは、リストの融合を改善し、リストの(逆)構築を少なくするコードの高速ハックです。

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ポイントの場合、初期バージョンの3.8秒に対して0.8秒で完了します

于 2009-01-22T09:38:18.513 に答える
10

私の 32 ビット x86 システムでは、プログラムは約 40 MB のメモリしか使用しません。

プロファイリング出力の「total alloc = 116,835,180 bytes」行と、プログラムが一度に実際に使用するメモリ量を混同していませんか? 合計割り当ては、プログラムの実行全体で割り当てられたメモリの量です。これらの多くは、処理を進めるにつれてガベージ コレクターによって解放されます。Haskell プログラムでは、その数が非常に大きくなると予想できます。実行中に数テラバイトのメモリを割り当てるプログラムがありますが、実際には最大仮想メモリ サイズは 100 メガバイト程度です。

プログラムの実行中に大量の割り当てが行われることについて、私はあまり心配しません。それが純粋言語の性質であり、GHC のランタイムには、これを補うのに役立つ非常に優れたガベージ コレクタがあります。

于 2009-06-06T08:44:15.107 に答える
4

より一般的には、 GHCのヒーププロファイリングツールを使用して、メモリがどこに向かっているのかを知ることができます。私の経験では、データが漏洩している理由を必ずしも教えてくれるわけではありませんが、少なくとも潜在的な原因を絞り込むことができます。

また、厳密性の理解、ガベージコレクションとの相互作用、および問題の診断と修正の方法について、DonStewartによるこの優れたブログ投稿に光を当てることもできます。

于 2009-01-31T15:41:37.263 に答える
2

$! を使用して「非怠惰」を強制しますか? ヘルプ?この回答に従って。

于 2009-01-20T06:08:13.073 に答える
1

Harleqin のリクエスト: 最適化フラグを設定してみましたか? たとえば、ghc では、gcc と同じように add "-O2" を使用できます。(ghc にどのような最適化レベルが存在するかはわかりませんが、man ページには正確には記載されていません ...)

私の過去の経験では、このフラグを設定することで大きな違いが生まれました。私の知る限り、runhugs最適化されていないghcHaskellの最も基本的で明白な実装を使用しています。残念ながら、これはあまり効率的でない場合があります。

しかし、それは単なる推測です。コメントで言ったように、誰かがあなたの質問にうまく答えてくれることを願っています。Haskell のメモリ使用量の分析と修正で問題が発生することがよくあります。

于 2009-01-20T06:29:39.597 に答える
1

スイッチ-fvia-Cも併用。

于 2009-01-20T13:27:57.383 に答える
0

私の目に飛び込んできたことの 1 つは、Haskell の出力が float であるのに対し、C の出力は整数のように見えることです。私はまだ Haskell コードを理解していませんが、C が整数を使用している間に、Haskell で浮動小数点演算を使用する場所がおそらくあるでしょうか?

于 2009-01-20T09:40:45.017 に答える