32

これらの単純なFooデータ構造が非常に多数 (数百万/数十億以上) あるとします。

data Foo = Foo
    { a :: {-# UNPACK #-}!Int
    , b :: Int
    }

これらが非常に多く出回っているため、それらが消費するメモリの量を考慮する必要があります。

64 ビット マシンでは、それぞれIntが 8 バイトであるため、a8 バイトしか使用しません (厳密でアンパックされているため)。しかし、どのくらいのメモリを占有するのbでしょうか? サンクが評価されるかどうかで変わると思いますよね?

評価が必要なb場合にのみメモリにとどまっているメモリ位置の数に依存する可能性があるため、一般的なケースではこれを伝えることは不可能だと思います。bしかしb、(いくつかの非常に高価な操作) のみに依存しているとしたらどうaでしょうか? それでは、どのくらいのメモリが使用されるかを決定論的に知る方法はありますか?

4

2 に答える 2

31

user239558 の回答に加えて、そこにあるあなたのコメントに応えて、値のヒープ表現を検査し、このような質問に対する回答を自分で見つけ、最適化の効果を確認し、コンパイルのさまざまな方法。

ghc-データサイズ

クロージャのサイズを示します。ここでは、(64 ビット マシン上で) 評価された形式でガベージ コレクション後Foo 1 2に、それ自体で 24 バイト、依存関係を含めて合計 40 バイトが必要であることがわかります。

Prelude GHC.DataSize Test> let x = Foo 1 2
前奏曲 GHC.DataSize Test> x
フー{a = 1、b = 2}
プレリュード GHC.DataSize テスト> System.Mem.performGC
プレリュード GHC.DataSize Test> ClosureSize x
24
プレリュード GHC.DataSize Test> recursiveSize x
40

これを再現するには、 を使用してコンパイル済みの形式でデータ定義をロードする必要があります。-Oそうしないと、{-# UNPACK #-}プラグマは効果がありません。

次に、サンクを作成して、サイズが大幅に増加することを確認します。

プレリュード GHC.DataSize Test> let thunk = 2 + 3::Int
Prelude GHC.DataSize Test> let x = Foo 1 サンク
前奏曲 GHC.DataSize Test> x `seq` return ()
プレリュード GHC.DataSize テスト> System.Mem.performGC
プレリュード GHC.DataSize Test> ClosureSize x
24
プレリュード GHC.DataSize Test> recursiveSize x
400

これはかなり過剰です。その理由は、この計算には静的クロージャやNum型クラス辞書などへの参照が含まれており、一般に GHCi バイトコードは最適化されていないためです。それでは、それを適切な Haskell プログラムに入れましょう。ランニング

main = do
    l <- getArgs
    let n = length l
    n `seq` return ()
    let thunk = trace "I am evaluated" $ n + n
    let x = Foo 1 thunk
    a x `seq` return ()
    performGC
    s1 <- closureSize x
    s2 <- closureSize thunk
    r <- recursiveSize x
    print (s1, s2, r)

を与える(24, 24, 48)ので、Foo値はFooそれ自体とサンクで構成されます。nなぜサンクだけなのか、さらに 16 バイトを追加することへの参照もあるはずがないのですか? これに答えるには、より優れたツールが必要です。

ghc-ヒープビュー

このライブラリ (私による) は、ヒープを調査し、データがそこでどのように表現されているかを正確に伝えることができます。したがって、上記のファイルに次の行を追加します。

buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree

(プログラムに 5 つのパラメーターを渡すと) 結果が得られFoo (_thunk 5) 1ます。ポインタは常にデータの前に来るため、引数の順序がヒープ上で入れ替わることに注意してください。プレーン5は、サンクのクロージャがその引数をアンボックス化して格納することを示します。

最後の演習として、サンクを遅延させてこれを検証しnます。

main = do
    l <- getArgs
    let n = length l
    n `seq` return ()
    let thunk = trace "I am evaluated" $ n
    let x = Foo 1 thunk
    a x `seq` return ()
    performGC
    s1 <- closureSize x
    s2 <- closureSize thunk
    s3 <- closureSize n
    r <- recursiveSize x
    buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree
    print (s1, s2, s3, r)

(コンストラクターの存在によって示されるように) Foo (_thunk (I# 4)) 1for の別のクロージャーを使用してのヒープ表現を提供し、値とその合計の予想されるサイズを示します。nI#(24,24,16,64)

ああ、これでもレベルが高すぎる場合は、getClosureRawで raw バイトが返されます。

于 2012-12-21T09:58:13.577 に答える
13

b評価されると、Intオブジェクトへのポインタになります。ポインタは8バイトで、Intオブジェクトは8バイトのヘッダーと8バイトのヘッダーで構成されてInt#います。

したがって、その場合、メモリ使用量はFooオブジェクト(8ヘッダー、Int8、8ポインター)+ボックス化Int(8ヘッダー、8 Int#)です。

が評価されていない場合b、の8バイトポインタはサンクオブジェクトFooを指します。Thunkオブジェクトは、未評価の式を表します。オブジェクトと同様に、このオブジェクトには8バイトのヘッダーがありますが、オブジェクトの残りの部分は、未評価の式の自由変数で構成されています。Int

したがって、まず、このサンクオブジェクトに保持される自由変数の数は、Fooオブジェクトを作成する式によって異なります。Fooを作成するさまざまな方法では、潜在的に異なるサイズのサンクオブジェクトが作成されます。

次に、自由変数は、クロージャの環境と呼ばれる、式の外部から取得された未評価の式で言及されているすべての変数です。これらは式のパラメータの一種であり、どこかに格納する必要があるため、サンクオブジェクトに格納されます。

したがって、Fooコンストラクターが呼び出される実際の場所を確認し、2番目のパラメーターの自由変数の数を確認してサンクのサイズを見積もることができます。

Thunkオブジェクトは、他のほとんどのプログラミング言語のクロージャと実際には同じですが、重要な違いが1つあります。評価されると、評価されたオブジェクトへのリダイレクトポインタによって上書きされる可能性があります。したがって、結果を自動的に記憶するクロージャです。

このリダイレクトポインタは、Intオブジェクト(16バイト)を指します。ただし、現在「デッド」なサンクは、次のガベージコレクションで削除されます。GCがFooをコピーすると、FooのbがIntオブジェクトを直接指すようになり、サンクが参照されなくなり、ガベージになります。

于 2012-12-21T04:38:49.940 に答える