Belajar Assembly - Optimasi Kinerja & Profiling
Episode 18 of 23

Belajar Assembly - Optimasi Kinerja & Profiling

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.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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.

Prinsip Pertama: Ukur Dulu

Optimasi tanpa pengukuran adalah tebakan. Siklus yang benar:

  1. Buat versi yang benar (baseline).
  2. Profiling dengan perf/gprof — temukan hot path.
  3. Optimasi satu hal, ukur ulang.
  4. Simpan yang lebih cepat; ulangi.

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.

Teknik Optimasi Level Instruksi

1. Loop Unrolling

Loop memiliki biaya tetap: dec, cmp, jmp. Dengan unrolling, badan loop diulang beberapa kali per iterasi sehingga biaya cabang dibagi:

Loop biasa (4 iterasi)
    mov rcx, 4
loop:
    add rax, [rbx]
    add rbx, 8
    loop loop
Unrolled (menghemat 3 cabang)
    mov rcx, 4
loop:
    add rax, [rbx]
    add rax, [rbx + 8]
    add rax, [rbx + 16]
    add rax, [rbx + 24]
    add rbx, 32
    loop loop

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

2. Register Allocation

Register lebih cepat dari memori — itu aksioma. Perhatikan pola akses berulang:

Akses memori berulang (lambat)
add rax, [rbx]      ; 3 akses memori
add rax, [rbx]
add rax, [rbx]
Load sekali (cepat)
mov rcx, [rbx]      ; 1 akses memori
add rax, rcx
add rax, rcx
add rax, rcx

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

3. Alignment: Data dan Instruksi

  • Data: field dan array harus aligned dengan ukurannya (kita bahas di episode 11). movaps butuh 16-byte; malalignment → SIGSEGV atau penalti besar.
  • Instruksi: meletakkan label loop pada boundary 16-byte bisa meningkatkan fetch/decode di beberapa CPU. GAS mendukung .balign 16; NASM align 16:
Align label loop
align 16
.loop:
    ...

4. Menghindari Branch Misprediction

CPU memprediksi arah jmp/jXX untuk menjaga pipeline tetap penuh. Saat prediksi salah, pipeline dibuang dan restart — penalti bisa puluhan cycle. Cara mengurangi:

  • Buat pola branch yang dapat diprediksi (miring ke satu arah).
  • Ganti branch dengan arithmetic: cmov (conditional move) untuk kode tanpa efek samping.
  • Hindari branch acak (data-driven) — pertimbangkan tabel lookup.
cmov vs branch
; tanpa branch: rax = (cond) ? a : b
cmp rdx, 0
cmove rax, rcx       ; rax = (rdx==0) ? rcx : rax

cmov menghindari misprediction sepenuhnya, tetapi selalu mengeksekusi kedua jalur — jika jalurnya mahal, branch biasa bisa lebih baik.

Profiling dengan perf

perf adalah profiler resmi Linux — mengukur hingga level instruksi:

Statistik performa
perf stat ./program
Output perf stat (ringkas)
   1,234,567,890  cycles
   2,345,678,901  instructions
   5.2           branches
   0.8%          branch-misses
   3.2           IPC

Perhatikan IPC (instructions per cycle): nilai ideal 3–4; di bawah 1 berarti ada bottleneck (cache miss, misprediction, dependency chain). Metrics kunci lain:

Profil hot spot
perf record ./program
perf report

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

Profiling dengan gprof

gprof adalah profiler berbasis instrumentasi — statis dan mudah dijalankan:

gprof
gcc -pg -O2 program.c -o program
./program            # menghasilkan gmon.out
gprof ./program | head -30

Kelebihan gprof: output tabel pemanggilan (call graph). Kekurangannya: instrumentasi mengubah perilaku, dan resolusi hanya di level fungsi — tidak sampai instruksi seperti perf.

Timing Presisi dengan rdtsc

Untuk benchmark mikro di level instruksi, rdtsc membaca Time Stamp Counter — counter cycle CPU:

CTiming rdtsc
#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.

Pitfalls Optimasi

  • Optimasi tanpa baseline — tidak tahu apakah lebih cepat.
  • Unroll berlebihan — binary membengkak, instruction cache terluka.
  • Mengabaikan cache line — data akses berurutan memenangkan cache; akses acak membunuh performa.
  • Branch data-driven — misprediction mahal; pertimbangkan cmov atau lookup table.
  • Mengukur salahrdtsc tanpa kalibrasi, atau compiler meng-optimasi habis benchmark (biasanya: hasil tidak dipakai → dibuang; buat hasil digunakan).
  • Menulis assembly untuk semua kode — hanya untuk hot path yang terbukti; sisanya biarkan compiler.

Penutup

Pada episode 18 ini, kalian telah belajar mengoptimasi dengan metode ilmiah.

Inti yang harus dibawa pulang:

  • Ukur dulu: perf stat → IPC, branch-misses; perf record/report → hot spot.
  • Loop unrolling, register allocation, dan alignment: hemat pipeline & cache.
  • Kurangi misprediction: pola branch konsisten, cmov untuk jalur murah.
  • gprof untuk call graph; rdtsc untuk timing level instruksi (dengan kalibrasi).
  • Optimasi hanya untuk hot path terbukti; compiler sering menang.

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.

Belajar Assembly - Optimasi Kinerja & Profiling | Belajar Assembly