Intro Compiler & Machine Abstraction
Catatan: Catatan ini merangkum dua topik yang saling berkaitan: optimisasi compiler dan abstraksi level mesin (ISA, virtual address, processor state x86-64). Materi mengikuti alur pembukaan Bab 3 CS:APP.
Compiler Optimization
Optimisasi adalah proses compiler mengubah kode sumber menjadi bentuk yang lebih cepat, lebih kecil, atau lebih hemat sumber daya, tanpa mengubah perilaku program yang teramati dari luar.
Programmer menulis kode yang mudah dibaca manusia; compiler menerjemahkannya menjadi kode mesin yang efisien.
Aturan dasar: as-if rule
Compiler boleh melakukan transformasi apa pun selama program berperilaku seolah-olah tidak diubah. Output akhir harus tetap sama.
Undefined behavior (UB) di C/C++ dapat menghasilkan perilaku yang tampak aneh justru karena aturan ini. Compiler berasumsi UB tidak pernah terjadi, lalu melakukan optimisasi berdasarkan asumsi tersebut. Kode yang "kelihatannya benar" bisa dihapus atau diubah total.
Jenis-jenis optimisasi
- Constant folding
- Ekspresi konstan dihitung saat kompilasi.
x = 2 * 3 + 4menjadix = 10. - Dead code elimination
- Kode yang tidak pernah dieksekusi, atau yang hasilnya tidak pernah dipakai, dibuang.
- Inlining
- Pemanggilan fungsi kecil diganti langsung dengan isi fungsinya, menghilangkan overhead call.
- Loop unrolling
- Badan loop "dibentangkan" untuk mengurangi overhead pengecekan kondisi dan lompatan.
- Common subexpression elimination
- Ekspresi yang dihitung berulang cukup dihitung satu kali.
- Vectorization (SIMD)
- Beberapa operasi data diproses sekaligus dalam satu instruksi, memanfaatkan vector register.
- Register allocation
- Variabel yang sering dipakai ditempatkan di register CPU, bukan di memori.
Tip: Register x86-64 hanya ada 16 buah. Keterbatasan inilah yang membuat register allocation menjadi salah satu masalah tersulit dalam desain compiler. Variabel yang tidak kebagian register harus di-spill ke stack, dan itu jauh lebih lambat.
Flag optimisasi
| Flag | Keterangan |
|---|---|
-O0 |
Tanpa optimisasi. Paling nyaman untuk debugging karena kode mesin masih memetakan rapi ke kode sumber. |
-O1 |
Optimisasi dasar. |
-O2 |
Standar untuk produksi. |
-O3 |
Lebih agresif, termasuk vectorization dan inlining yang lebih luas. |
-Os |
Mengutamakan ukuran biner yang kecil. |
Di Rust, mode optimisasi diaktifkan dengan cargo build --release.
Tip: Saat debugging, gunakan -O0. Pada level optimisasi tinggi, variabel bisa hilang dari debugger, urutan eksekusi berubah, dan breakpoint tidak mendarat di tempat yang diharapkan.
Abstraksi dalam Sistem Komputer
Abstraksi adalah menyembunyikan detail implementasi di balik model yang lebih sederhana. Untuk pemrograman level mesin, ada dua abstraksi yang paling penting.
1. Instruction Set Architecture (ISA)
ISA adalah "kontrak" antara perangkat keras dan perangkat lunak. ISA mendefinisikan:
- Processor state — apa saja yang disimpan prosesor (register, program counter, condition flag)
- Format instruksi — bagaimana instruksi dikodekan
- Efek tiap instruksi terhadap state tersebut
Sebagian besar ISA, termasuk x86-64, mendeskripsikan perilaku program seolah-olah setiap instruksi dieksekusi berurutan: satu instruksi selesai sebelum instruksi berikutnya dimulai.
Kenyataan di perangkat keras
Prosesor nyata jauh lebih rumit dan mengeksekusi banyak instruksi secara konkuren:
- Pipelining — beberapa tahap instruksi tumpang tindih
- Superscalar — beberapa instruksi dieksekusi bersamaan
- Out-of-order execution — urutan eksekusi disusun ulang sesuai kesiapan data
- Speculative execution — prosesor menebak hasil percabangan sebelum diketahui
Prosesor menggunakan pengaman (dependency checking, reorder buffer, in-order commit) untuk memastikan hasil akhirnya tetap identik dengan model sekuensial yang dijanjikan ISA.
Catatan: Pola yang sama muncul di dua lapisan berbeda. Compiler boleh mengubah urutan kode (as-if rule), hardware boleh mengubah urutan eksekusi — keduanya sah selama perilaku yang teramati tidak berubah.
2. Virtual Address
Alamat memori yang dipakai program level mesin adalah alamat virtual, yang menyajikan memori sebagai satu array byte yang sangat besar dan berurutan.
Implementasi sebenarnya jauh lebih kompleks:
- Hierarki perangkat keras: register → cache L1/L2/L3 → DRAM → disk
- Perangkat lunak sistem operasi: page table, MMU, TLB, penanganan page fault, swapping
Sistem operasi mengelola ruang alamat virtual dan menerjemahkan alamat virtual menjadi alamat fisik.
Ukuran ruang alamat x86-64
- Alamat direpresentasikan sebagai word 64-bit
- Pada implementasi saat ini, 16 bit teratas harus bernilai nol
- Ruang alamat efektif = 248 byte ≈ 64 terabyte
- Program biasa hanya memakai beberapa megabyte sampai beberapa gigabyte
Pada satu waktu, hanya sebagian kecil rentang alamat virtual yang dianggap valid. Akses ke alamat yang tidak valid inilah yang memunculkan segmentation fault.
Abstraksi yang bocor (leaky abstraction)
Kedua abstraksi di atas membuat assembly bisa dipelajari sama sekali. Namun abstraksi tersebut bocor dalam beberapa situasi:
- Performa — ISA menjanjikan urutan, tetapi cache miss dan branch misprediction tetap memengaruhi kecepatan nyata
- Konkurensi — model sekuensial tidak lagi cukup ketika beberapa thread berinteraksi, sehingga diperlukan memory barrier dan operasi atomik
- Keamanan — Spectre dan Meltdown adalah kasus di mana pengaman ternyata tidak sempurna: speculative execution meninggalkan jejak di cache yang bisa dibaca penyerang
Representasi Level Mesin
Compiler mengerjakan bagian terberat: menerjemahkan model eksekusi C yang relatif abstrak menjadi instruksi prosesor yang sangat elementer.
Assembly dan machine code pada dasarnya sama; perbedaannya hanya representasi. Assembly berbentuk teks yang bisa dibaca manusia, machine code berbentuk biner. Hubungan keduanya nyaris satu-ke-satu.
Processor state yang terlihat
Bagian-bagian state prosesor berikut tersembunyi dari programmer C, tetapi terlihat jelas di level mesin.
Program counter (%rip)
Menyimpan alamat memori instruksi berikutnya yang akan dieksekusi. Di C, posisi eksekusi adalah konsep implisit; di level mesin, ini register konkret yang dapat diubah lewat instruksi lompat dan pemanggilan fungsi.
Integer register file
16 lokasi bernama, masing-masing menyimpan nilai 64-bit. Isinya dapat berupa alamat (setara pointer di C) atau data integer.
| Register | Peran umum (System V AMD64 ABI) |
|---|---|
%rax |
Nilai kembalian fungsi |
%rbx |
Callee-saved |
%rcx |
Argumen ke-4 |
%rdx |
Argumen ke-3 |
%rsi |
Argumen ke-2 |
%rdi |
Argumen ke-1 |
%rbp |
Callee-saved / frame pointer |
%rsp |
Stack pointer |
%r8 |
Argumen ke-5 |
%r9 |
Argumen ke-6 |
%r10, %r11 |
Caller-saved, data sementara |
%r12–%r15 |
Callee-saved |
Sebagian register melacak bagian penting dari state program, sebagian lain menampung data sementara seperti argumen, variabel lokal, dan nilai kembalian.
Condition code register
Menyimpan informasi status hasil operasi aritmetika atau logika yang terakhir dieksekusi (zero flag, sign flag, carry, overflow).
Register inilah yang dipakai untuk mengimplementasikan perubahan alur kontrol bersyarat seperti if dan while:
- Instruksi pembanding dieksekusi → flag berubah
- Instruksi lompat bersyarat membaca flag → alur program bercabang
Catatan: Satu pernyataan if di C selalu "dicicil" menjadi minimal dua langkah terpisah di level mesin: penetapan flag, lalu pembacaan flag.
Vector register
Setiap register dapat menampung satu atau lebih nilai integer atau floating-point sekaligus. Inilah dasar perangkat keras untuk SIMD dan vectorization.
Memori: tipe data adalah ilusi
C menyediakan model di mana objek dengan tipe berbeda dideklarasikan dan dialokasikan di memori. Kode mesin memandang memori semata-mata sebagai array byte besar yang dapat dialamati.
Konsekuensinya:
- Tipe agregat seperti array dan struct direpresentasikan sebagai kumpulan byte yang bersebelahan
- Assembly tidak membedakan integer signed dan unsigned
- Assembly tidak membedakan jenis pointer satu sama lain
- Assembly bahkan tidak membedakan pointer dari integer
Yang membedakan signed dan unsigned bukan datanya, melainkan instruksi mana yang dipilih untuk mengolahnya. Pola bit-nya identik. Inilah sebabnya kesalahan tipe data di C bisa lolos tanpa gejala sampai jauh di kemudian hari.
Tip: Hal ini juga menjelaskan mengapa casting pointer di C praktis "gratis" di level mesin — tidak ada data yang berubah, hanya cara compiler memperlakukannya.
Isi program memory
Ruang memori program berisi:
- Kode mesin yang dapat dieksekusi
- Informasi yang dibutuhkan sistem operasi
- Run-time stack untuk mengelola pemanggilan dan pengembalian prosedur
- Blok memori yang dialokasikan pengguna, misalnya melalui
malloc
Seluruhnya dialamati menggunakan alamat virtual.
Satu instruksi = satu operasi elementer
Satu instruksi mesin hanya melakukan operasi yang sangat sederhana, misalnya:
- Menjumlahkan dua angka yang tersimpan di register
- Memindahkan data antara memori dan register
- Melompat ke alamat instruksi lain secara bersyarat
Compiler harus menyusun rangkaian instruksi semacam itu untuk mengimplementasikan konstruksi program seperti evaluasi ekspresi aritmetika, loop, serta pemanggilan dan pengembalian prosedur.
Catatan: Inilah gambaran besarnya. C menyediakan objek bertipe, variabel bernama, dan struktur kontrol. Mesin hanya punya 16 register, array byte, flag, dan instruksi sederhana. Jarak antara keduanya diisi oleh compiler — dan membaca hasil pengisian jarak itulah inti dari Bab 3 CS:APP.