Pilihan gaya penulisan di Kotlin memengaruhi waktu eksekusi dan jumlah alokasi memori, walaupun hasil logikanya sama. Topik ini membahas tiga titik pengaruh yang paling sering muncul, yaitu fungsi inline yang menghilangkan pemanggilan tambahan dan objek lambda, pemrosesan Sequence pada data besar yang menghindari daftar perantara, serta StringBuilder yang menghindari penyalinan berulang saat menyusun string. Ketiganya dijalankan sebagai satu program, dan keluarannya sengaja memakai kategori waktu alih-alih angka mentah agar hasilnya dapat direproduksi.
Satu program, tiga pengukuran#
Program berikut menjalankan ketiga pengukuran secara berurutan pada satu main. Fungsi kategoriWaktu menerjemahkan hasil measureTimeMillis menjadi tiga kategori yang lebar, yaitu di bawah 100 ms, 100 sampai 1000 ms, dan lebih dari 1000 ms. Pembagian kategori ini disengaja, karena angka milidetik mentah berubah-ubah antar eksekusi dan antar mesin, sedangkan kategori yang lebarnya belasan kali lipat datanya tetap stabil. Nilai yang bersifat logika, seperti panjang string dan jumlah akhir, dicetak persis karena selalu sama.
import kotlin.system.measureTimeMillis
inline fun prosesTiap(nilai: List<Int>, blok: (Int) -> Unit) {
for (x in nilai) blok(x)
}
fun kategoriWaktu(ms: Long): String = when {
ms < 100 -> "di bawah 100 ms"
ms < 1000 -> "100 sampai 1000 ms"
else -> "lebih dari 1000 ms"
}
fun demoInline() {
val data = listOf(1, 2, 3, 4, 5, 6)
prosesTiap(data) {
if (it == 4) return // menghentikan demoInline, bukan hanya lambda
print("$it ")
}
}
fun main() {
demoInline()
println("(demoInline berhenti pada 4)")
// bagian sequence: data besar tanpa daftar perantara
val besar = (1..1_000_000).toList()
val tahap1 = besar.filter { it % 2 == 1 }
val tahap2 = tahap1.map { it * 3 }
val tahap3 = tahap2.filter { it % 7 == 0 }
println("eager : perantara ${besar.size} -> ${tahap1.size} -> ${tahap2.size} -> ${tahap3.size}")
println("eager : jumlah ${tahap3.sumOf { it.toLong() }}")
val totalSequence = besar.asSequence()
.filter { it % 2 == 1 }
.map { it * 3 }
.filter { it % 7 == 0 }
.sumOf { it.toLong() }
println("sequence: jumlah $totalSequence")
// bagian alokasi: konkatenasi string dalam perulangan vs StringBuilder
val potongan = List(12_000) { "kata-$it" }
var teks = ""
val waktuConcat = measureTimeMillis {
for (p in potongan) teks += p
}
println("konkatenasi : panjang ${teks.length}, ${kategoriWaktu(waktuConcat)}")
val pembangun = StringBuilder()
val waktuBuilder = measureTimeMillis {
for (p in potongan) pembangun.append(p)
}
println("StringBuilder: panjang ${pembangun.length}, ${kategoriWaktu(waktuBuilder)}")
}# compiler Kotlin dan pustaka standar yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi main.kt menjadi out.jar, lalu jalankan programnya
kotlinc main.kt -d out.jar && java -cp "out.jar:$STDLIB" MainKt1 2 3 (demoInline berhenti pada 4)
eager : perantara 1000000 -> 500000 -> 500000 -> 71429
eager : jumlah 107144142861
sequence: jumlah 107144142861
konkatenasi : panjang 108890, 100 sampai 1000 ms
StringBuilder: panjang 108890, di bawah 100 msSetiap bagian program meninggalkan jejaknya pada baris keluaran yang berbeda. Baris pertama berasal dari fungsi demoInline, yang menjadi contoh pembahasan inline pada bagian berikutnya, yaitu angka 1 sampai 3 dicetak sebelum perulangannya berhenti pada 4. Dua baris eager dan satu baris sequence berasal dari pembandingan data besar, sedangkan dua baris terakhir berasal dari perbandingan penyusunan string. Urutan keluarannya tetap pada setiap eksekusi, sehingga pembahasan pada bagian-bagian berikutnya dapat merujuk ke keluaran yang sama.
Fungsi inline#
Fungsi tingkat tinggi tanpa inline menyimpan lambda argumennya sebagai objek fungsi, lalu menjalankannya lewat pemanggilan antarmuka, sehingga setiap pemanggilan membawa biaya alokasi objek dan lompatan pemanggilan. Modifier inline mengubah perilaku itu dengan menempelkan badan fungsi dan badan lambda argumennya langsung ke titik pemanggilan, sehingga kedua biaya tersebut tidak terjadi. Efek sampingnya yang mudah diamati terlihat pada baris pertama keluaran, yaitu return di dalam lambda berhasil menghentikan fungsi demoInline sehingga angka 4 sampai 6 tidak pernah dicetak, sedangkan main tetap berlanjut.
Perhentian semacam itu disebut non-local return dan hanya mungkin pada lambda yang badannya ditempel langsung, karena pada fungsi tanpa inline lambda berjalan di objek sendiri sehingga return biasa di dalamnya ditolak compiler. Dua kata kunci lain mengatur perilaku ini secara lebih halus, yaitu noinline yang membuat satu parameter lambda tetap menjadi objek, dan crossinline yang melarang non-local return pada lambda yang dijalankan dari konteks lain. Keduanya cukup diingat sebagai penanda bahwa perilaku default parameter lambda adalah ditempel penuh.
Sequences pada data besar#
Baris eager pada keluaran memperlihatkan harga dari pemrosesan collection biasa pada satu juta elemen. Pemanggilan filter pertama membentuk daftar berisi 500000 elemen, map membentuk daftar perantara berikutnya berukuran sama, dan filter kedua membentuk daftar akhir berisi 71429 elemen. Ketiga daftar perantara itu dialokasikan di memori, dan setiap elemennya tersimpan sebagai objek Integer tersendiri. Baris sequence menghitung jumlah yang persis sama, yaitu 107144142861, tanpa satu pun daftar perantara, karena elemen mengalir satu per satu melewati seluruh rantai operator.
Pada pengukuran berulang di sandbox ini, versi eager menyelesaikan rantai yang sama dalam waktu sekitar satu setengah sampai dua kali lipat versi sequence, dan angka mentahnya sengaja tidak dicetak karena bergantung pada mesin dan kondisi saat pengukuran. Keuntungan serupa membesar ketika rantai operatornya makin panjang atau ketika operator terminalnya berhenti lebih awal, sedangkan untuk data kecil dengan satu atau dua operator, biaya pengelolaan iterator sequence justru dapat melebihi penghematannya.
Kesadaran alokasi dengan StringBuilder#
Dua baris terakhir keluaran membandingkan cara menyusun string dari 12000 potongan. Ekspresi teks += p tidak mengubah string lama, melainkan membuat string baru yang menyalin seluruh isi lama ditambah potongan barunya, sehingga total penyalinan tumbuh kuadratik terhadap jumlah iterasi. StringBuilder menyimpan isi pada penyangga internal yang diperbesar sesekali, sehingga seluruh proses hanya menyalin setiap karakter dalam jumlah yang wajar. Keduanya menghasilkan panjang yang sama, yaitu 108890 karakter, tetapi kategori waktunya terpencar, yaitu 100 sampai 1000 ms untuk konkatenasi dan di bawah 100 ms untuk StringBuilder, dengan selisih minimal sepuluh kali lipat.
Perbandingan ini memperlihatkan makna kesadaran alokasi secara umum. Setiap objek yang dibuat menimbulkan kerja alokasi memori dan pembersihannya, dan objek berumur pendek memang murah, tetapi jumlahnya yang besar tetap terasa pada waktu eksekusi. Sebelum mengubah kode demi performa, ukur dahulu bagian yang benar-benar lambat, karena penghematan pada bagian yang jarang berjalan tidak terasa sama sekali.
Ringkasan#
- Fungsi inline menempelkan badannya dan badan lambda argumennya ke titik pemanggilan, sehingga objek lambda dan pemanggilan tambahan tidak terjadi, dan
returndi dalam lambda dapat menghentikan fungsi di sekitarnya. Sequencememproses elemen satu per satu tanpa daftar perantara, sehingga menghemat alokasi pada data besar dan rantai operator yang panjang.StringBuildermenyusun string lewat penyangga internal, sedangkan konkatenasi+=menyalin ulang seluruh isi pada setiap iterasi hingga waktunya tumbuh kuadratik.- Angka waktu mentah berubah-ubah antar eksekusi, sehingga pengukuran pada topik ini dicetak sebagai kategori yang lebar dan hasil logikanya dicetak persis.
Referensi#
- Inline functions — Kotlin Documentation
— Mekanisme inline, non-local return, serta peran
noinlinedancrossinline. - Sequences — Kotlin Documentation
— Evaluasi lazy pada
Sequencedan kondisi ketika sequences memberi keuntungan.