Jangan mengejek branch predictor yang bahagia
- Baru-baru ini sering menulis assembly AArch64
- Ide "cerdas" untuk menghapus satu lompatan dalam loop justru menurunkan performa
- Kesalahan ini dijelaskan agar orang lain tidak mengulangi kesalahan yang sama
Contoh kode
float run(const float* data, size_t n) {
float g = 0.0;
while (n) {
n--;
const float f = *data++;
foo(f, &g);
}
return g;
}
static void foo(float f, float* g) {
// g를 수정하는 작업
}
Diterjemahkan ke assembly AArch64
// x0: const float* data
// x1: size_t n
// s0: 반환할 float
stp x29, x30, [sp, #-16]!
mov s0, #0.0
loop:
cmp x1, #0
b.eq exit
sub x1, x1, #1
ldr s1, [x0], #4
bl foo
b loop
foo:
// s1에서 읽고 s0에 누적
// ...
ret
exit:
ldp x29, x30, [sp], #16
ret
Percobaan optimasi
- Mencoba meningkatkan performa dengan mengurangi instruksi
bl
- Namun performanya justru menurun
Perbandingan performa
- Kode asli: 969 ns
- Kode teroptimasi: 3.85 µs
Analisis penyebab
- Branch predictor menjadi bingung karena pasangan
bl dan ret tidak cocok
- Menurut dokumentasi ARM, instruksi
ret membantu memprediksi pengembalian fungsi
Cara mengatasinya
- Gunakan
br x30 alih-alih ret
- Performa pulih: 913 ns
Optimasi tambahan
- Inline
foo untuk meningkatkan performa
- Gunakan loop unrolling dan instruksi SIMD
Performa akhir
- SIMD + loop unrolling manual: 94 ns
Kesimpulan
- Jangan membuat branch predictor bingung
- Kode SIMD lebih cepat, tetapi hasilnya bisa berbeda karena penjumlahan floating-point tidak mengikuti sifat asosiatif
Opini GN⁺
- Artikel ini menunjukkan dengan baik pentingnya optimasi assembly AArch64
- Memahami cara kerja branch predictor sangat penting untuk optimasi performa
- Optimasi dengan instruksi SIMD sangat efektif, tetapi masalah akurasi perlu dipertimbangkan
- Menggunakan bahasa tingkat tinggi seperti Rust dapat mempermudah peningkatan performa melalui optimasi kompiler
- Proyek dengan fungsi serupa antara lain panduan optimasi assembly dari Agner Fog
1 komentar
Pendapat Hacker News
Meringkas artikelnya bersama teman-teman dari era Apple II
Raymond Chen membahas topik yang sama hampir 20 tahun lalu
Pada kode SIMD, penjumlahan floating point dapat dilakukan dalam urutan berbeda karena penjumlahan floating point tidak memenuhi sifat asosiatif
Sejak Rust 1.78, kompiler menggunakan loop unrolling yang lebih agresif dan sedikit SIMD
Bingung bagaimana
x0bertambah di assembly ARM/ARM64ldr s1, [x0], #4melakukan load sambil menaikkanx0sebesar 4Mengejutkan bahwa tidak dicoba cara yang kurang kompleks untuk mengoptimalkan kode assembly
foobisa di-inline dan instruksiRETbisa dihilangkanAda pendapat bahwa sebaiknya penulis tidak terus-menerus mengganti satuan