Belajar Scala - Performance & Optimization
Episode 21 of 23

Belajar Scala - Performance & Optimization

Mengoptimalkan aplikasi Scala berbasis data: menghindari alokasi berlebihan, memilih Array versus collection, memahami JVM warmup dan tuning GC, lalu profiling dengan JFR dan async-profiler serta benchmark presisi memakai JMH lewat sbt-jmh.

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

Pendahuluan

Setelah episode 20 membahas versi dan fitur, sekarang kita masuk ranah yang paling sering disalahpahami: performa. Banyak developer melakukan optimasi prematur — menebak-nebak mana yang lambat, menghindari fitur bahasa, atau menulis kode jelek demi kecepatan yang tidak terbukti. Episode ini mengajarkan pendekatan yang benar: ukur dulu, optimasi yang terbukti.

Mengapa penting? Karena aplikasi Scala yang lambat jarang karena Scala-nya, melainkan karena pola yang boros: alokasi berlebihan, struktur data yang salah, atau GC yang tidak dipahami. Dan sekali lagi, kecepatan tidak bisa ditebak — harus diukur dengan tool yang tepat.

Aturan Pertama: Jangan Optimasi Prematur

Aturan emas performance engineering:

  1. Ukur — profiling menemukan bottleneck yang nyata, bukan yang diduga.
  2. Optimasi — perbaiki titik yang terbukti mahal.
  3. Ukur lagi — buktikan perbaikan; hindari "optimasi" yang tidak terukur.

JVM sudah sangat cepat untuk kode yang ditulis wajar. Yang membunuh performa adalah pola tertentu yang mahal — dan itulah fokus kita.

Menghindari Alokasi Berlebihan

Alokasi objek adalah musuh kinerja utama di JVM: semakin banyak objek, semakin sering GC bekerja. Contoh pola boros:

Pola boros alokasi
def process(numbers: Array[Int]): Int =
  numbers.toList          // salinan ke List
    .map(_ * 2)           // List baru
    .filter(_ > 10)       // List baru lagi
    .sum                  // boxing ke objek

Untuk data berukuran besar, setiap langkah mengalokasikan. Versi yang lebih hemat untuk koleksi besar:

Varian yang lebih hemat
def process(numbers: Array[Int]): Int =
  var total = 0
  var i = 0
  while i < numbers.length do
    val doubled = numbers(i) * 2
    if doubled > 10 then total += doubled
    i += 1
  total

Kedua versi benar; yang kedua jauh lebih hemat alokasi. Tetapi — dan ini penting — untuk ukuran data kecil (ratusan elemen), perbedaannya tak terukur. Optimasi semacam ini hanya layak setelah profiling membuktikan hot path-nya ada di sini.

Warning

Prioritas pertama harus tetap keterbacaan dan kebenaran. Menulis kode berantakan untuk kecepatan yang tidak diukur adalah kerugian ganda: susah dipelihara dan tidak terbukti lebih cepat. Alokasi-averse style dipakai hanya di hot path yang sudah terverifikasi.

Array vs Collections

Pilihan struktur data (episode 6) berdampak besar pada performa:

AspekArrayList/Vector
MemoriKontigu, tanpa boxing (untuk primitif)Node terpisah / tree, boxing
Akses acakO(1)List O(n), Vector O(log n)
AlokasiMinimumSetiap operasi
Kapan dipakaiHot path, data besar, interop JavaKode idiomatis umum
Array untuk hot path numerik
val data = Array.ofDim[Double](1_000_000)
var i = 0
while i < data.length do
  data(i) = i * 1.5
  i += 1

Array[Double] menyimpan primitif secara kontigu — tanpa boxing, tanpa overhead. Ini pilihan untuk perhitungan numerik besar. Untuk kode bisnis biasa, List/Vector tetap lebih tepat: lebih aman, lebih ekspresif.

JVM Warmup dan Tuning

JVM melakukan JIT compilation — kode diinterpretasi dulu, lalu dioptimalkan saat sering dieksekusi. Konsekuensinya:

  • Warmup: aplikasi JVM "menghangat" — performa stabil setelah beberapa menit beban.
  • Benchmark yang benar harus melakukan warmup sebelum mengukur (inilah yang dilakukan JMH di bawah).
  • Tuning GC bisa mengubah dunia untuk aplikasi dengan alokasi besar.
Contoh flag GC untuk aplikasi Java/Scala
java -Xms2g -Xmx2g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=100 \
  -jar my-app.jar

G1GC adalah default modern yang menyeimbangkan throughput dan pause. Untuk aplikasi dengan heap besar, -Xmx yang benar (tidak terlalu kecil → GC terus-menerus; tidak terlalu besar → pause panjang) adalah tuning pertama yang paling berdampak.

Profiling: JFR dan async-profiler

Jangan menebak — ukur. Dua tool utama di 2026:

JFR (Java Flight Recorder)

JFR adalah profiler bawaan JDK, overhead sangat rendah:

Rekam JFR saat aplikasi berjalan
java -XX:StartFlightRecording=filename=app.jfr,duration=60s -jar my-app.jar
Analisis file jfr
jfr summary app.jfr
jfr view alloc app.jfr   # lihat alokasi terbesar
jfr view hot-methods app.jfr

JFR menjawab pertanyaan kunci: metode mana yang paling banyak memakan CPU? Alokasi objek terbesar berasal dari mana? Dari sinilah kalian tahu hot path yang sebenarnya.

async-profiler

async-profiler memakai teknik perf_event_open + JVMTI untuk flame graph dengan overhead minimal:

Async profiler dengan flamegraph
./profiler.sh -d 30 -f flame.html <PID>

Flame graph langsung menunjukkan stack frames yang dominan secara visual. Instalasi di Linux biasanya butuh perf_event_paranoid yang diatur:

Izinkan profiling (per-linux)
sudo sysctl kernel.perf_event_paranoid=1

Benchmark dengan JMH

JMH (Java Microbenchmark Harness) adalah standar benchmark JVM — ia mengatasi warmup, dead-code elimination, dan noise. Integrasi dengan sbt via plugin sbt-jmh:

project/plugins.sbt
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")
Di build.sbt
enablePlugins(JmhPlugin)
Benchmark sederhana
import org.openjdk.jmh.annotations.*
 
@State(Scope.Benchmark)
class SumBench:
 
  val numbers: Array[Int] = (1 to 100_000).toArray
 
  @Benchmark
  def viaWhile(): Long =
    var total = 0L
    var i = 0
    while i < numbers.length do
      total += numbers(i)
      i += 1
    total
 
  @Benchmark
  def viaFold(): Long =
    numbers.foldLeft(0L)(_ + _)

Jalankan:

Jalankan benchmark
sbt "jmh:run -i 5 -wi 5 SumBench"

JMH menjalankan 5 iterasi warmup dan 5 iterasi ukuran, lalu melaporkan throughput per benchmark:

Output JMH
Benchmark          Mode  Cnt       Score      Error  Units
SumBench.viaFold   thrpt   5   58325.343  ± 412.11  ops/s
SumBench.viaWhile  thrpt   5  189034.221  ± 902.40  ops/s

Hasil ini baru berbicara — viaWhile terbukti ~3x lebih cepat untuk array besar. Dengan angka ini, keputusan optimasi bisa dipertanggungjawabkan.

Tip

Alur kerja yang benar: JMH untuk membandingkan dua implementasi secara adil, JFR/async-profiler untuk menemukan bottleneck di aplikasi nyata. Mulai dari profiling aplikasi (temukan masalah), lalu turun ke JMH (buktikan perbaikan) — bukan sebaliknya.

Common Pitfalls

  • Benchmark tanpa warmup — hasil diukur di fase JIT yang belum stabil; selalu pakai JMH atau manual warmup.
  • Dead-code elimination — hasil benchmark yang tidak dipakai (mis. tanpa mengakumulasi ke nilai) bisa dibuang JVM; gunakan Blackhole/konsumsi hasil.
  • -Xmx raksasa — heap besar bukan otomatis cepat; GC pause ikut membesar. Tuning butuh pengukuran, bukan insting.
  • Mengubah kode demi performa tanpa data — ukur dulu; sering kali bottleneck ada di tempat yang tak terduga (I/O, serialisasi, query).

Penutup

Inti yang harus dibawa pulang:

  • Ukur → optimasi → ukur lagi; jangan optimasi prematur tanpa data.
  • Alokasi berlebihan adalah musuh utama; kurangi hanya di hot path yang terbukti.
  • Array untuk data besar & numerik; List/Vector untuk kode idiomatis.
  • JVM butuh warmup; tuning GC (-Xmx, G1) berdampak besar.
  • JFR & async-profiler untuk profiling aplikasi; JMH (sbt-jmh) untuk benchmark adil.

Di episode 22, episode terakhir series ini, kita merangkum semuanya: ekosistem, alternatif, dan refleksi akhir — membandingkan Scala dengan Kotlin, Java, Haskell, dan Clojure, kapan memilih yang mana, plus checklist produksi dan sumber belajar resmi. Sampai jumpa di episode 22!