Pertanyaan Berapa banyak memori yang digunakan?


Katakanlah saya memiliki angka yang sangat besar (jutaan / miliar +) dari yang sederhana ini Foo struktur data:

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

Dengan begitu banyak yang beredar, menjadi perlu untuk memikirkan berapa banyak memori yang mereka konsumsi.

Pada mesin 64-bit, masing-masing Int adalah 8 byte, jadi a hanya membutuhkan 8 byte (karena sangat ketat dan tidak dibongkar). Tetapi berapa banyak memori akan b mengambil? Saya membayangkan ini akan berubah tergantung pada apakah bunyi itu dievaluasi atau tidak, bukan?

Saya membayangkan dalam kasus umum ini tidak mungkin untuk diceritakan, karena b dapat bergantung pada sejumlah posisi memori yang hanya tinggal di memori untuk berjaga-jaga b perlu dievaluasi. Tetapi bagaimana jika b hanya bergantung pada (beberapa operasi yang sangat mahal) a? Lalu, apakah ada cara deterministik untuk mengetahui berapa banyak memori yang akan digunakan?


32
2017-12-21 00:57


asal


Jawaban:


Selain jawaban pengguna239558, dan sebagai tanggapan atas komentar Anda di sana, saya ingin menunjukkan beberapa alat yang memungkinkan Anda memeriksa representasi nilai Anda, menemukan jawaban untuk pertanyaan seperti ini sendiri dan untuk melihat efek pengoptimalan dan cara kompilasi yang berbeda.

ghc-datasize

memberitahu Anda ukuran penutupan. Di sini Anda dapat melihat bahwa (pada mesin 64 bit) dalam bentuk yang dievaluasi dan setelah pengumpulan sampah, Foo 1 2 membutuhkan 24 byte sendiri dan, termasuk dependensi, 40 byte secara total:

Prelude GHC.DataSize Test> biarkan x = Foo 1 2
Prelude GHC.DataSize Test> x
Foo {a = 1, b = 2}
Prelude GHC.DataSize Test> System.Mem.performGC
Prelude GHC.DataSize Test> closureSize x
24
Prelude GHC.DataSize Test> rekursifUkuran x
40

Untuk mereproduksi ini Anda perlu memuat definisi data dalam bentuk dikompilasi dengan -O, jika tidak, {-# UNPACK #-} pragma tidak berpengaruh.

Sekarang mari kita buat sebuah thunk dan melihat bahwa ukurannya naik secara signifikan:

Prelude GHC.DataSize Test> biarkan thunk = 2 + 3 :: Int
Prelude GHC.DataSize Test> biarkan x = Foo 1 thunk
Prelude GHC.DataSize Test> x `seq` return ()
Prelude GHC.DataSize Test> System.Mem.performGC
Prelude GHC.DataSize Test> closureSize x
24
Prelude GHC.DataSize Test> rekursifUkuran x
400

Sekarang ini sangat berlebihan. Alasannya adalah bahwa perhitungan ini termasuk referensi ke penutupan statis, Num kamus typeclass dan sejenisnya, dan umumnya bytecode GHCi sangat tidak dioptimalkan. Jadi mari kita bahas itu dalam program Haskell yang tepat. Lari

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)

memberi (24, 24, 48), jadi sekarang Foo nilai terdiri dari Foo sendiri dan seorang thunk. Kenapa hanya thunk, seharusnya tidak ada referensi untuk itu n menambahkan 16 byte lainnya? Untuk menjawab ini, kita membutuhkan alat yang lebih baik:

ghc-heap-view

Perpustakaan ini (oleh saya) dapat menyelidiki tumpukan dan memberi tahu Anda secara tepat bagaimana data Anda diwakili di sana. Jadi tambahkan baris ini ke file di atas:

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

kita dapatkan (ketika kita melewati lima parameter ke program) hasilnya Foo (_thunk 5) 1. Perhatikan bahwa urutan argumen bertukar di heap, karena pointer selalu datang sebelum data. Datarannya 5menunjukkan bahwa penutupan toko thunk argumennya tidak dikotak-kotak.

Sebagai latihan terakhir, kami memverifikasi ini dengan membuat orang malas malas masuk n: Sekarang

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)

memberikan representasi heap Foo (_thunk (I# 4)) 1 dengan penutupan terpisah untuk n (seperti yang ditunjukkan oleh kehadiran I# konstruktor) dan menunjukkan ukuran yang diharapkan untuk nilai dan totalnya, (24,24,16,64).

Oh, dan jika ini masih terlalu tinggi, getClosureRaw memberi Anda byte mentah.


31
2017-12-21 09:58



Jika b dievaluasi, itu akan menjadi penunjuk ke Int obyek. Penunjuk adalah 8 byte, dan Int objek terdiri dari header yang 8 byte, dan Int# yaitu 8 byte.

Jadi dalam hal ini penggunaan memori adalah Foo objek (8 tajuk, 8 Int, 8 penunjuk) + kotak Int (8 tajuk, 8 Int#).

Kapan b tidak dievaluasi, pointer 8-byte masuk Foo akan menunjuk ke a Benda aneh. Itu Benda aneh mewakili ekspresi yang tidak terevaluasi. Seperti Int objek, objek ini memiliki header 8-byte, tetapi sisa objek terdiri dari variabel bebas dalam ekspresi yang tidak dievaluasi.

Jadi pertama-tama, jumlah variabel bebas yang disimpan dalam objek thunk ini tergantung pada ekspresi yang menciptakan objek Foo. Berbagai cara untuk menciptakan Foo akan memiliki objek-objek yang berbeda dari ukuran yang berbeda-beda.

Lalu kedua, itu variabel bebas adalah semua variabel yang disebutkan dalam ekspresi yang tidak dievaluasi yang diambil dari luar ekspresi, yang disebut lingkungan penutupan. Mereka adalah semacam parameter untuk ekspresi dan mereka perlu disimpan di suatu tempat, dan dengan demikian mereka disimpan dalam objek thunk.

Jadi Anda dapat melihat tempat-tempat yang sebenarnya di mana konstruktor Foo dipanggil dan melihat jumlah variabel bebas dalam parameter kedua untuk memperkirakan ukuran thunk.

Objek A Thunk benar-benar sama dengan penutupan di sebagian besar bahasa pemrograman lainnya, dengan satu perbedaan penting. Ketika dievaluasi, itu dapat ditimpa oleh pointer redirect ke objek yang dievaluasi. Jadi itu adalah penutupan yang secara otomatis memaafkan hasilnya.

Pengarah pengalihan ini akan mengarah ke Int objek (16 byte). Namun sekarang thunk "mati" akan dihilangkan pada pengumpulan sampah berikutnya. Ketika GC menyalin Foo, itu akan membuat Foo b titik langsung ke objek Int, membuat si brengsek tidak dirujuk dan dengan demikian sampah.


13
2017-12-21 04:38