Pengukuran performa dimulai dari dua pertanyaan, yaitu seberapa cepat program berjalan dan di bagian mana waktunya habis. Topik ini menjawab keduanya pada program penghitung deret bernama hitung: build debug dibandingkan dengan build release untuk melihat efek optimasi compiler, opsi crystal build dibaca dari --help untuk memastikan makna setiap opsi, lalu alur profiling dan pembacaan flame graph dijelaskan sebagai cara menemukan bagian program yang paling boros waktu. Program yang sama dijalankan pada kedua mode build dengan keluaran yang identik, sehingga perbandingannya tidak terganggu perbedaan perilaku.
Program pembanding#
Program pembanding diberi pekerjaan yang tetap, yaitu menjumlahkan seluruh bilangan bulat dari 1 sampai sepuluh juta, kemudian mencetak hasil akhirnya saja. Pekerjaan yang selalu sama membuat keluarannya stabil, sedangkan hasil tunggal tanpa keluaran lain membuat perbandingan antarmode build mudah dibaca.
# menjumlahkan seluruh bilangan bulat dari 1 sampai 10.000.000
batas = 10_000_000
total = 0_i64
angka = 1
while angka <= batas
total += angka
angka += 1
end
puts totalPenampung total bertipe Int64 karena hasil penjumlahannya melebihi jangkauan Int32. Perulangan while dipilih agar pekerjaan per iterasi sesederhana mungkin, sehingga efek optimasi compiler terkonsentrasi pada satu lintasan eksekusi. Binary dibangun dengan crystal build, dan opsi -o menentukan nama berkas binary yang dihasilkan.
# build binary versi debug, yaitu tingkat optimasi default compiler
crystal build -o hitung hitung.cr
# jalankan binary hasil build debug
./hitung50000005000000Build tanpa --release menggunakan tingkat optimasi 0 yang menjadi nilai default -O, sesuai daftar opsi pada crystal build --help. Mode inilah yang digunakan selama pengembangan karena waktu kompilasinya singkat, sehingga siklus sunting, build, dan jalankan tetap cepat. Hasil eksekusinya sudah benar, yaitu 50000005000000, walaupun kodenya belum melalui optimasi apa pun.
Build release dan perbandingannya#
Build release dihasilkan dengan menambahkan opsi --release. Kedua binary dibangun dari berkas sumber yang sama, kemudian dijalankan berurutan dengan pekerjaan yang sama untuk menunjukkan bahwa keluarannya identik.
# build binary versi release dengan optimasi penuh
crystal build --release -o hitung-release hitung.cr
# jalankan kedua binary dan bandingkan keluarannya
./hitung
./hitung-release50000005000000
50000005000000Kedua baris keluaran sama, artinya build release tidak mengubah perilaku program, melainkan cara kodenya dieksekusi. Komprominya terletak pada dua hal yang berlawanan arah: waktu kompilasi menjadi lebih lama karena compiler menjalankan tahap optimasi tambahan, sedangkan kecepatan eksekusi untuk pekerjaan berat menjadi lebih tinggi. Pada program kecil seperti hitung, selisihnya sulit dirasakan tanpa alat ukur, tetapi perbedaan keduanya tetap dapat dinyatakan secara kualitatif pada tabel berikut.
| Aspek | Build debug | Build release |
|---|---|---|
| Opsi build | tanpa --release | --release |
| Tingkat optimasi | 0 (default) | 3 sekaligus --single-module |
| Waktu kompilasi | lebih singkat | lebih lama |
| Kecepatan eksekusi | dasar | lebih cepat untuk pekerjaan berat |
| Fase yang cocok | pengembangan sehari-hari | distribusi dan pengukuran |
Tingkat optimasi pada tabel diambil langsung dari definisi resmi opsi compiler, yaitu -O dengan default 0 dan --release yang dinyatakan sebagai -O3 --single-module. Praktik yang lazim adalah mengembangkan program pada build debug, kemudian membangun versi release ketika program akan didistribusikan atau diukur.
Membaca opsi pada crystal build#
Definisi setiap opsi build tidak perlu ditebak karena crystal build --help mencetaknya seluruhnya. Keluaran berikut diambil dari compiler versi yang digunakan pada jalur ini.
# tampilkan seluruh opsi yang dimiliki crystal build
crystal build --helpUsage: crystal build [options] [programfile] [--] [arguments]
Options:
--cross-compile cross-compile
-d, --debug Add full symbolic debug info
--no-debug Skip any symbolic debug info
--frame-pointers auto|always|non-leaf
Control the preservation of frame pointers
-D FLAG, --define FLAG Define a compile-time flag
--emit [asm|obj|llvm-bc|llvm-ir]
Comma separated list of types of output for the compiler to emit
--x86-asm-syntax att|intel X86 dialect for --emit=asm: AT&T (default), Intel
-f text|json, --format text|json Output format: text (default), json
--error-trace Show full error trace
-h, --help Show this message
--ll Dump ll to Crystal's cache directory
--link-flags FLAGS Additional flags to pass to the linker
--mcpu CPU Target specific cpu type
--mattr CPU Target specific features
--mcmodel MODEL Target specific code model
--warnings all|none Which warnings to detect. (default: all)
--error-on-warnings Treat warnings as errors.
--exclude-warnings <path> Exclude warnings from path (default: lib)
--no-color Disable colored output
--no-codegen Don't do code generation
-o FILE, --output FILE Output path. If a directory, the filename is derived from the first source file (default: ./)
--prelude Use given file as prelude
--release Compile in release mode (-O3 --single-module)
-O LEVEL Optimization mode: 0 (default), 1, 2, 3, s, z
-s, --stats Enable statistics output
-p, --progress Enable progress output
-t, --time Enable execution time output
--single-module Generate a single LLVM module
--threads NUM Maximum number of threads to use
--target TRIPLE Target triple
--verbose Display executed commands
--static Link statically
--stdin-filename Source file name to be read from STDINBeberapa opsi pada daftar tersebut relevan untuk topik ini. Opsi --release membawa dua hal sekaligus, yaitu tingkat optimasi 3 dan --single-module yang menggabungkan seluruh kode menjadi satu modul LLVM sehingga optimasi antarfungsi dapat berlangsung. Opsi -O menyetel tingkat optimasi secara manual dengan nilai lain dari daftar 0, 1, 2, 3, s, z, opsi --no-debug membuang informasi debug dari binary, sedangkan opsi --emit mengeluarkan hasil antara seperti llvm-ir untuk diperiksa ketika perilaku compiler perlu dibedah lebih lanjut.
Profiling dan flame graph#
Build release menjawab seberapa cepat program berjalan, sedangkan profiling menjawab di bagian mana waktunya habis. Profiling adalah pengukuran yang mengambil sampel eksekusi secara berkala, kemudian mengelompokkan sampel tersebut per fungsi, sehingga fungsi yang paling sering tampil menjadi kandidat utama optimasi. Alat yang lazim digunakan pada Linux adalah perf, dan valgrind menyediakan pendekatan lain melalui instrumentasi. Ketersediaan keduanya diperiksa lebih dahulu karena tidak semua lingkungan memuatnya.
# periksa ketersediaan perf di lingkungan kerja
command -v perf || echo "perf tidak tersedia di lingkungan ini"
# periksa ketersediaan valgrind di lingkungan kerja
command -v valgrind || echo "valgrind tidak tersedia di lingkungan ini"perf tidak tersedia di lingkungan ini
valgrind tidak tersedia di lingkungan iniLingkungan kerja ini tidak memuat perf maupun valgrind, sehingga dua blok berikutnya hanya menunjukkan alurnya dan tidak dieksekusi. Alurnya dimulai dari perekaman profil pada binary release, kemudian perekaman tersebut dirangkum per fungsi.
# tidak-dijalankan
# merekam profil eksekusi binary release ke berkas perf.data
perf record -o perf.data ./hitung-release
# merangkum fungsi yang paling banyak menghabiskan waktu
perf report -i perf.dataUntuk flame graph, data rekaman diubah menjadi teks polos, dilipat per tumpukan pemanggilan, lalu digambar menjadi berkas SVG oleh sepasang skrip dari kumpulan alat FlameGraph yang dipasang terpisah.
# tidak-dijalankan
# mengubah rekaman profil menjadi teks polos
perf script -i perf.data > perf.script
# menggambar flame graph dari data yang sudah dilipat
stackcollapse-perf.pl perf.script | flamegraph.pl > flamegraph.svgFlame graph dibaca sebagai susunan kotak yang mewakili tumpukan pemanggilan fungsi. Setiap kotak menamai satu fungsi, kedalaman susunan menyatakan rantai pemanggilannya, dan lebar kotak menyatakan proporsi sampel yang melewati fungsi tersebut. Lebar bukan durasi berurutan, sehingga kotak yang lebar berarti banyak sampel terkumpul di situ dan itulah tempat pertama yang layak dioptimasi.
Kesalahan umum#
- Menyimpulkan kecepatan program dari satu eksekusi yang berlangsung sangat singkat. Durasi sesingkat itu mudah digeser oleh faktor lain, misalnya beban sistem pada saat pengukuran. Ulangi eksekusi beberapa kali dan bandingkan pola hasilnya sebelum mengambil kesimpulan.
- Membandingkan dua build dengan pekerjaan yang berbeda, misalnya batas deret yang berbeda pula. Selisih waktu yang terlihat kemudian berasal dari pekerjaannya, bukan dari mode buildnya. Gunakan berkas sumber yang sama dengan pekerjaan yang tetap, sebagaimana
hitung.crpada topik ini. - Membaca lebar kotak flame graph sebagai linimasa eksekusi. Sumbu datarnya menyatakan proporsi sampel, bukan urutan waktu, sehingga kotak yang bersebelahan belum tentu dijalankan berurutan. Baca lebar kotak sebagai besarnya kontribusi waktu.
- Mengoptimasi dua bagian program sekaligus pada satu putaran pengukuran. Efek kedua perubahan menjadi tidak dapat dipisahkan, sehingga kontribusi masing-masing tidak diketahui. Lakukan satu perubahan, ukur ulang, baru lanjut ke perubahan berikutnya.
Ringkasan#
crystal buildtanpa--releasemenggunakan tingkat optimasi default dan cocok untuk pengembangan, sedangkan--releasemenghasilkan binary yang dioptimasi penuh untuk distribusi dan pengukuran.- Keluaran program tidak berubah antara build debug dan build release, yang berbeda hanyalah waktu kompilasi dan kecepatan eksekusinya.
crystal build --helpmemuat definisi resmi setiap opsi, termasuk--releaseyang dinyatakan sebagai-O3 --single-module.- Profiling mengumpulkan sampel eksekusi per fungsi, dan flame graph memvisualisasikannya sehingga fungsi paling boros waktu terbaca dari lebar kotaknya.
Referensi#
- Referensi bahasa Crystal 1.22 — Crystal Lang — Panduan resmi compiler Crystal dan opsi build yang dibahas pada topik ini.
- API Crystal 1.22.1 — Crystal Lang
— Dokumentasi pustaka standar yang mendasari program hitung beserta tipe
Int64pada penampungnya.