Belajar Haskell - Performance & Optimization
Episode 21 of 23

Belajar Haskell - Performance & Optimization

Mengoptimalkan kode Haskell secara ilmiah: memahami strictness analysis dan unlifted types, menghindari space leak akibat akumulasi lazy, lalu memakai profiler GHC dengan cabal build --enable-profiling dan +RTS -p, heap profile untuk menemukan kebocoran memori, serta benchmark yang andal dengan Criterion.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Sampai episode 20 kalian membangun aplikasi yang benar dan aman. Episode 21 ini menjawab pertanyaan yang selalu datang berikutnya: bagaimana membuatnya cepat — dan yang lebih penting, bagaimana mengetahuinya secara ilmiah, bukan dengan firasat.

Mengapa bab ini penting? Karena di Haskell, "optimasi dengan feeling" sering keliru arah: laziness membuat intuisi tentang biaya jadi menyesatkan. Kalian tidak bisa menebak di mana bottleneck berada — kalian harus mengukur. Episode ini mengajarkan disiplin profiling: strictness analysis untuk memahami, profiler untuk menemukan, dan Criterion untuk membuktikan perbaikan.

Mental Model Biaya di Haskell

Di bahasa imperatif, biaya ≈ jumlah statement. Di Haskell, biaya dipengaruhi tiga hal yang tidak terlihat: alokasi (boxed vs unlifted), thunk (evaluasi tertunda), dan GC. Memori short-lived sangat murah di Haskell (GC generational), tetapi thunk yang menumpuk dan alokasi yang bocor ke generasi tua adalah biaya tersembunyi terbesar.

Strictness Analysis: GHC Berpikir untuk Kalian

Compiler GHC melakukan strictness analysis: ia membuktikan parameter mana yang pasti dibutuhkan, lalu membuatnya strict tanpa mengubah semantik. Ini artinya banyak kode "lazy" di-highlight oleh compiler menjadi se-efisien versi eager — kalian sering mendapat performa tanpa mengubah apa pun.

GHC meneliti strictness
add :: Int -> Int -> Int
add x y = x + y
-- x dan y pasti dibutuhkan -> GHC membuatnya strict

Cek dengan flag -ddump-stranal untuk melihat hasil analisis ini. Pesan pentingnya: optimalkan setelah mengukur, karena GHC sudah mengoptimalkan banyak hal sendiri.

Unlifted Types: Menghindari Box

Nilai Int biasa adalah boxed: pointer ke heap yang berisi nilai. Unlifted types (Int#) menyimpan nilai langsung di register — tanpa alokasi heap. Kode "hot loop" yang menuntut kecepatan maksimal bisa memakainya lewat MagicHash, tetapi harganya: kode menjadi jauh lebih berbahaya (bisa crash jika salah).

Int# - unlifted, tanpa alokasi
{-# LANGUAGE MagicHash #-}
 
import GHC.Exts
 
siklusCepat :: Int# -> Int#
siklusCepat 0# = 0#
siklusCepat n  = n +# siklusCepat (n -# 1#)

Aturan praktis: jangan mulai dari sini. Gunakan Int biasa, profil, dan hanya jika profiler membuktikan hot path-nya, pertimbangkan unlifted. Sebagian besar kode production tidak pernah membutuhkannya.

Space Leak: Musuh Utama Performa

Kita sudah menyinggung space leak di episode 7. Di sini kita melihatnya dari sisi performa: space leak = thunk menumpuk = memori naik tanpa henti = GC bekerja keras = program melambat.

Pola paling umum:

Space leak: akumulasi lazy
-- BERBAHAYA: foldl membangun rantai thunk
jumlahLambat :: [Int] -> Int
jumlahLambat = foldl (+) 0
 
-- AMAN: foldl' mengevaluasi akumulator tiap langkah
import Data.List (foldl')
 
jumlahCepat :: [Int] -> Int
jumlahCepat = foldl' (+) 0

Pola lain yang kerap bocor:

  • Akumulasi string dengan ++ di kiri: acc ++ "x" di dalam loop → struktur O(n²) dan thunk. Gunakan list kanan + reverse, atau Text dengan Data.Text.Builder.
  • length lalu !! berulang pada list yang sama — list dijelajahi ganda.
  • Map/Set lazy yang menumpuk thunk di nilai — gunakan Data.Map.Strict.

Warning

Gejala space leak: program berjalan makin lambat seiring waktu, penggunaan memori naik monoton, dan +RTS -s menunjukkan residensi memori jauh di atas yang dibutuhkan. Jangan "memperbaiki" dengan menebak — konfirmasi dulu dengan heap profile di bawah, baru perbaiki pola yang terbukti bermasalah.

Profiling dengan GHC Profiler

Profiling di GHC terdiri dari dua level: time profiling (di mana waktu dihabiskan) dan heap profiling (di mana memori dihabiskan).

Time Profiling: +RTS -p

Build dengan profiling diaktifkan:

Build dengan profiling
cabal build --enable-profiling
./app +RTS -p

Perintah menghasilkan file app.prof yang menampilkan cost centre — fungsi mana yang menyumbang waktu paling besar:

Cuplikan app.prof
COST CENTRE  MODULE   SRC            %time
main         Main     Main.hs:1:1     45.6
jumlahLambat Data.List               30.1

+RTS -s menambahkan ringkasan runtime:

Statistik runtime
./app +RTS -s

Menampilkan total alokasi, residensi maksimum, dan waktu GC — ukuran pertama sebelum masuk terlalu dalam.

Heap Profile: Menemukan Space Leak

Heap profile menunjukkan apa yang mendominasi memori. Profil per biografi (alokasi vs survival) bisa mengungkap thunk yang bertahan:

Heap profile per biografi
./app +RTS -hT
# menghasilkan app.hp, render ke grafik
hp2ps -c app.hp

Grafik app.ps (atau versi PDF) menunjukkan bagaimana heap bertumbuh. Pola yang mencurigakan: penggunaan memori yang naik linier padahal data masukan konstan — tanda thunk menumpuk. Biografi SURVIVED yang tinggi menunjukkan data yang seharusnya short-lived justru dipertahankan.

Tip

Workflow profiling yang benar: (1) ukur baseline dengan +RTS -s, (2) buat heap profile untuk melihat jenis memori, (3) perbaiki satu pola yang terbukti (biasanya fold/akumulasi lazy), (4) ukur ulang dan bandingkan. Satu perubahan per iterasi — kalian harus tahu perubahan mana yang menghasilkan efek.

Benchmark dengan Criterion

Profiler menjawab "di mana" dan "mengapa"; benchmark menjawab "berapa banyak" — dan mengunci agar regresi terdeteksi. Criterion adalah benchmark library standar dengan statistik yang jujur:

Benchmark dengan Criterion
import Criterion.Main
 
slowSum :: [Int] -> Int
slowSum = foldl (+) 0
 
fastSum :: [Int] -> Int
fastSum = foldl' (+) 0
 
main :: IO ()
main = defaultMain
    [ bench "foldl (lazy)" $ whnf slowSum (take 1000000 [1..])
    , bench "foldl' (strict)" $ whnf fastSum (take 1000000 [1..])
    ]

Jalankan dengan:

Jalankan benchmark
cabal bench

Criterion menjalankan banyak iterasi, menghitung rata-rata dengan interval kepercayaan, dan menampilkan perbandingan:

Hasil benchmark
benchmarking foldl' (strict)
time                 9.104 ms   (8.9 ms .. 9.3 ms)
                     0.999 R²   (0.998 R² .. 1.000 R²)
mean                 9.142 ms

Perhatikan whnf — argumen dievaluasi ke WHNF (Weak Head Normal Form), standar untuk benchmark fungsi murni. Untuk data struktur yang perlu dievaluasi penuh, gunakan nf.

Benchmark yang Menyesatkan

Kesalahan umum benchmark Haskell:

  1. Mengukur kode yang tidak dievaluasi — dengan laziness, hasil bisa "tidak pernah dihitung"; gunakan whnf/nf dengan benar.
  2. Input terlalu kecil — efek di bawah noise; ukur dengan input yang representatif.
  3. Optimasi compiler menghilangkan kerja-O2 + benchmark kosong bisa menghasilkan "0 detik"; pastikan hasil benar-benar dibutuhkan (cetak ke output yang di-blackhole).
  4. Membandingkan versi berbeda GHC/flag — benchmark hanya adil pada environment yang sama.

Checklist Optimasi yang Sehat

LangkahAlatKapan
Ukur baseline+RTS -sselalu sebelum optimasi
Temukan hot spot+RTS -psaat ada fungsi mencurigakan
Cari space leak+RTS -hTmemori naik tanpa henti
Buktikan perbaikanCriterionsebelum & sesudah setiap perubahan
Awasi regresiCI benchmarksetiap PR

Urutan ini mencegah kesalahan terbesar di optimasi Haskell: "mengoptimalkan hal yang tidak lambat" atau "memperbaiki yang tidak rusak".

Kesalahan Umum (Common Pitfalls)

  1. Optimasi sebelum mengukur — ubah kebiasaan: ukur dulu, tebak belakangan.
  2. foldl untuk akumulasi — ganti foldl'; space leak paling umum.
  3. String concatenation berulang — pakai Text/ByteString untuk workload besar.
  4. String untuk parsing besarText jauh lebih cepat dan hemat memori.
  5. Unlifted types sejak awal — berbahaya dan jarang perlu; profil dulu.
  6. Benchmark tanpa whnf — hasil tidak mencerminkan biaya nyata.

Penutup

Inti yang harus dibawa pulang:

  • Strictness analysis membuat GHC mengoptimasi banyak hal otomatis — jangan menebak, ukur dulu.
  • Unlifted types (Int#) untuk hot path ekstrem — gunakan hanya saat profiler membuktikan perlu.
  • Space leak = thunk menumpuk; lawan dengan foldl', struktur data strict, Text.
  • Profiling: cabal build --enable-profiling, +RTS -p (waktu), +RTS -hT (heap).
  • Criterion untuk benchmark yang jujur dan mengunci regresi.

Di episode 22 — episode terakhir series ini — kita akan membahas ekosistem, alternatif & refleksi akhir — membandingkan Haskell dengan Scala, Rust, Elm, dan PureScript, kapan memilih bahasa mana, rekap keseluruhan episode 0-21, checklist production, serta sumber belajar resmi untuk melanjutkan. Sampai jumpa di episode terakhir!

Belajar Haskell - Performance & Optimization | Belajar Haskell