Tahap performa

Build release, profiling, dan membaca flame graph untuk mengukur performa.

Daftar isi

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.

crystal hitung.cr
# 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 total

Penampung 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.

bash
# build binary versi debug, yaitu tingkat optimasi default compiler
crystal build -o hitung hitung.cr

# jalankan binary hasil build debug
./hitung
text Keluaran
50000005000000

Build 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.

bash
# build binary versi release dengan optimasi penuh
crystal build --release -o hitung-release hitung.cr

# jalankan kedua binary dan bandingkan keluarannya
./hitung
./hitung-release
text Keluaran
50000005000000
50000005000000

Kedua 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.

AspekBuild debugBuild release
Opsi buildtanpa --release--release
Tingkat optimasi0 (default)3 sekaligus --single-module
Waktu kompilasilebih singkatlebih lama
Kecepatan eksekusidasarlebih cepat untuk pekerjaan berat
Fase yang cocokpengembangan sehari-haridistribusi 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.

bash
# tampilkan seluruh opsi yang dimiliki crystal build
crystal build --help
text Keluaran
Usage: 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 STDIN

Beberapa 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.

bash
# 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"
text Keluaran
perf tidak tersedia di lingkungan ini
valgrind tidak tersedia di lingkungan ini

Lingkungan 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.

bash
# 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.data

Untuk 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.

bash
# 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.svg

Flame 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.cr pada 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 build tanpa --release menggunakan tingkat optimasi default dan cocok untuk pengembangan, sedangkan --release menghasilkan 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 --help memuat definisi resmi setiap opsi, termasuk --release yang 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#