Membuat kode assembly benar-benar cepat: loop unrolling, alokasi register yang tepat, alignment instruksi dan data, serta cara menghindari branch misprediction. Termasuk praktik profiling dengan perf stat/record, gprof, dan timing presisi memakai rdtsc.

Kalian sudah bisa menulis assembly yang benar — sekarang saatnya menulis assembly yang cepat. Episode 18 membedah teknik optimasi level instruksi dan cara mengukur dampaknya secara ilmiah dengan profiler. Jangan salah paham: mayoritas kode produksi seharusnya C/Rust dengan compiler -O2; assembly tangan hanya layak untuk hot path yang terbukti lewat profiling. Episode ini mengajarkan cara membuktikannya.
Mengapa penting? Karena CPU modern itu kompleks: pipeline, branch prediction, cache — dan optimasi naif sering memperlambat program. Memahami mengapa loop unrolling kadang membantu dan kadang tidak, dan mengapa branch misprediction mahal, membuat kalian tidak tertipu insting.
Optimasi tanpa pengukuran adalah tebakan. Siklus yang benar:
perf/gprof — temukan hot path.Warning
Hukum pertama optimasi: jangan. Hukum kedua (untuk kalian yang melanggar): ukur dulu. Tanpa benchmark yang benar, "perbaikan" kalian bisa jadi penurunan. Selalu bandingkan sebelum-sesudah dengan alat ukur yang sama.
Loop memiliki biaya tetap: dec, cmp, jmp. Dengan unrolling, badan loop diulang beberapa kali per iterasi sehingga biaya cabang dibagi:
mov rcx, 4
loop:
add rax, [rbx]
add rbx, 8
loop loop mov rcx, 4
loop:
add rax, [rbx]
add rax, [rbx + 8]
add rax, [rbx + 16]
add rax, [rbx + 24]
add rbx, 32
loop loopManfaatnya bukan cuma hemat 3 instruksi cabang — dengan unrolling, pipeline dan out-of-order execution melihat lebih banyak pekerjaan independen, sehingga utilasi CPU naik. Tapi unrolling terlalu agresif memperbesar binary dan bisa menyakiti instruction cache. Compiler (dengan #pragma unroll atau -O3) biasanya melakukannya lebih baik daripada tangan.
Register lebih cepat dari memori — itu aksioma. Perhatikan pola akses berulang:
add rax, [rbx] ; 3 akses memori
add rax, [rbx]
add rax, [rbx]mov rcx, [rbx] ; 1 akses memori
add rax, rcx
add rax, rcx
add rax, rcxTerutama hindari menulis ke memori di dalam loop (store forwarding + dependency chain). Inilah mengapa compiler memakai banyak register dan mengapa kita punya 16 di x86-64.
movaps butuh 16-byte; malalignment → SIGSEGV atau penalti besar..balign 16; NASM align 16:align 16
.loop:
...CPU memprediksi arah jmp/jXX untuk menjaga pipeline tetap penuh. Saat prediksi salah, pipeline dibuang dan restart — penalti bisa puluhan cycle. Cara mengurangi:
cmov (conditional move) untuk kode tanpa efek samping.; tanpa branch: rax = (cond) ? a : b
cmp rdx, 0
cmove rax, rcx ; rax = (rdx==0) ? rcx : raxcmov menghindari misprediction sepenuhnya, tetapi selalu mengeksekusi kedua jalur — jika jalurnya mahal, branch biasa bisa lebih baik.
perf adalah profiler resmi Linux — mengukur hingga level instruksi:
perf stat ./program 1,234,567,890 cycles
2,345,678,901 instructions
5.2 branches
0.8% branch-misses
3.2 IPCPerhatikan IPC (instructions per cycle): nilai ideal 3–4; di bawah 1 berarti ada bottleneck (cache miss, misprediction, dependency chain). Metrics kunci lain:
perf record ./program
perf reportperf record + perf report menunjukkan fungsi/instruksi mana yang memakan waktu terbanyak — titik fokus optimasi kalian.
Note
perf butuh akses ke perf_event_paranoid; di sistem aman mungkin perlu sudo atau mengatur kernel.perf_event_paranoid=2 via sysctl. Di container tanpa izin, gunakan gprof atau timing manual dengan rdtsc.
gprof adalah profiler berbasis instrumentasi — statis dan mudah dijalankan:
gcc -pg -O2 program.c -o program
./program # menghasilkan gmon.out
gprof ./program | head -30Kelebihan gprof: output tabel pemanggilan (call graph). Kekurangannya: instrumentasi mengubah perilaku, dan resolusi hanya di level fungsi — tidak sampai instruksi seperti perf.
Untuk benchmark mikro di level instruksi, rdtsc membaca Time Stamp Counter — counter cycle CPU:
#include <stdint.h>
static inline uint64_t rdtsc(void) {
unsigned int lo, hi;
__asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
void bench(void) {
uint64_t t0 = rdtsc();
// ... kode yang diukur ...
uint64_t t1 = rdtsc();
printf("cycles: %lu\n", t1 - t0);
}Warning
rdtsc mengukur cycle, bukan waktu dinding — nilainya bergantung pada frekuensi turbo dan perpindahan core. Untuk angka yang stabil: jalankan banyak iterasi, buang outlier (min), dan kalibrasi dengan cpuid/lfence untuk serialisasi. Untuk benchmark antar-fungsi yang wajar, clock_gettime(CLOCK_MONOTONIC) lebih portabel.
cmov atau lookup table.rdtsc tanpa kalibrasi, atau compiler meng-optimasi habis benchmark (biasanya: hasil tidak dipakai → dibuang; buat hasil digunakan).Pada episode 18 ini, kalian telah belajar mengoptimasi dengan metode ilmiah.
Inti yang harus dibawa pulang:
perf stat → IPC, branch-misses; perf record/report → hot spot.cmov untuk jalur murah.gprof untuk call graph; rdtsc untuk timing level instruksi (dengan kalibrasi).Di episode 19 selanjutnya, kalian menyatukan dunia assembly dan C secara penuh: interop — linking dengan C dan libraries (glibc) — memanggil printf dari assembly dengan stack alignment yang benar, linking via gcc, pemahaman GOT/PLT dan position-independent code, serta pembuatan shared library dari kode assembly. ABI episode 9 sekarang dipakai untuk bersaing dengan compiler di liganya sendiri.