Topik ini memetakan tiga komponen lanskap compiler produksi yang sering disebut namun jarang dibandingkan secara langsung, yaitu GCC sebagai compiler multi-pass klasik, Clang sebagai front end C dan C++ yang menargetkan LLVM, dan JIT (just-in-time compilation) sebagai penerjemah yang berjalan saat runtime. Pembaca memperoleh demo kompilasi GCC pada dua tingkat optimasi sehingga dapat membandingkan keluaran assembly dengan proyek kursus yang menargetkan stack VM, serta penjelasan posisi Clang dan JIT pada peta compiler. Setelah membaca topik ini, pembaca dapat menjelaskan perbedaan compiler AOT (ahead-of-time) produksi dari compiler proyek kursus, menyebutkan peran Clang pada ekosistem LLVM, dan menempatkan JIT pada spektrum penerjemah yang sudah diperkenalkan pada topik taksonomi.
GCC sebagai compiler multi-pass#
GCC adalah compiler produksi yang dipakai untuk membangun kernel Linux, distribusi Debian, dan banyak perangkat lunak bebas. Struktur GCC mengikuti pola front end dan back end dengan IR sebagai batas, sebagaimana dibahas pada modul representasi-antara, tetapi jumlah pass di dalamnya jauh lebih banyak daripada proyek kursus. Front end C menerjemahkan sumber menjadi AST, lalu diturunkan menjadi IR bernama GIMPLE yang berbentuk kode tiga alamat. GIMPLE dikonversi ke bentuk SSA (static single assignment) agar analisis aliran data menjadi lebih mudah, kemudian pass optimasi menjalankan inlining, propagasi konstanta, dan eliminasi kode mati pada SSA. Setelah optimasi tingkat tinggi, IR diturunkan menjadi RTL (register transfer language) yang lebih dekat ke mesin, lalu back end menurunkan RTL menjadi assembly untuk arsitektur sasaran, termasuk x86-64, ARM, dan RISC-V.
Banyaknya pass pada GCC merupakan akibat langsung dari persyaratan produksi. Compiler proyek kursus cukup menerjemahkan AST ke bytecode stack VM dalam satu pass codegen, sebab sasaran utamanya adalah pembelajaran konsep, bukan kinerja kode produksi. GCC harus mengoptimalkan kode untuk jutaan baris C yang sudah ditulis puluhan tahun, sehingga setiap pass dirancang untuk satu transformasi spesifik dan dapat dinyalakan atau dimatikan secara terpisah. Tingkat optimasi -O0 hingga -O3 pada GCC menetapkan kombinasi pass mana yang dijalankan. Pada -O0, mayoritas pass optimasi dimatikan sehingga keluaran assembly mudah dipetakan ke sumber, sedangkan pada -O2 dan -O3 sebagian besar pass dinyalakan, termasuk vektorisasi loop pada -O3.
Demo: kompilasi C pada dua tingkat optimasi#
Demo berikut menjalankan gcc --version untuk memastikan GCC tersedia, lalu mengompilasi program C kecil yang menjumlahkan bilangan dari 1 hingga n dengan dua tingkat optimasi, yaitu -O0 dan -O2. Badan fungsi sum diekstrak dari kedua keluaran assembly agar perbedaan visual antara kode tanpa optimasi dan kode yang sudah dioptimasi terlihat langsung. Jumlah byte kedua berkas assembly dilaporkan sebagai ukuran kasar keluaran.
"""Demo compiler produksi: GCC sebagai compiler multi-pass.
Skrip ini menjalankan `gcc --version` untuk memastikan GCC tersedia,
lalu memanggil `gcc -O0 -S` dan `gcc -O2 -S` pada program C kecil
yang menjumlahkan bilangan 1..n. Badan fungsi `sum` diekstrak dari
kedua keluaran assembly untuk memperlihatkan perbedaan visual antara
kode tanpa optimasi dan kode yang sudah dioptimasi. Jumlah byte
kedua berkas assembly dibandingkan sebagai ukuran kasar keluaran.
Pemakaian:
python3 gcc_demo.py
"""
import subprocess
import sys
import tempfile
from pathlib import Path
SMALL_C = """\
int sum(int n) {
int s = 0;
for (int i = 1; i <= n; i++) {
s = s + i;
}
return s;
}
int main(void) {
return sum(10);
}
"""
def run(cmd: list[str]) -> subprocess.CompletedProcess:
return subprocess.run(cmd, capture_output=True, text=True, check=True)
def extract_body(text: str) -> list[str]:
"""Ambil baris dari `sum:` hingga `ret` pertama setelahnya."""
lines = text.splitlines()
out: list[str] = []
started = False
for line in lines:
if not started:
if line.startswith("sum:"):
started = True
out.append(line)
continue
out.append(line)
if line.strip() == "ret":
break
return out
def main() -> None:
# pastikan GCC tersedia di sandbox
ver = run(["gcc", "--version"])
print("=== gcc --version ===")
sys.stdout.write(ver.stdout)
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "sum.c"
src.write_text(SMALL_C)
# kompilasi ke assembly pada dua tingkat optimasi
out_O0 = run(["gcc", "-S", "-O0", "-o", "-", str(src)])
out_O2 = run(["gcc", "-S", "-O2", "-o", "-", str(src)])
body_O0 = extract_body(out_O0.stdout)
body_O2 = extract_body(out_O2.stdout)
bytes_O0 = len(out_O0.stdout.encode("utf-8"))
bytes_O2 = len(out_O2.stdout.encode("utf-8"))
print()
print("=== Sumber sum.c ===")
sys.stdout.write(SMALL_C)
print()
print("=== Perintah ===")
print("gcc -S -O0 -o - sum.c")
print("gcc -S -O2 -o - sum.c")
print()
print("=== Badan fungsi sum pada -O0 ===")
for line in body_O0:
print(line)
print()
print("=== Badan fungsi sum pada -O2 ===")
for line in body_O2:
print(line)
print()
print("=== Perbandingan ukuran assembly ===")
print(f"ukuran assembly -O0 : {bytes_O0} byte")
print(f"ukuran assembly -O2 : {bytes_O2} byte")
if __name__ == "__main__":
main()Keluaran berikut diperoleh dengan menjalankan python3 gcc_demo.py pada sandbox dengan GCC 14.
=== gcc --version ===
gcc (Debian 14.2.0-19) 14.2.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
=== Sumber sum.c ===
int sum(int n) {
int s = 0;
for (int i = 1; i <= n; i++) {
s = s + i;
}
return s;
}
int main(void) {
return sum(10);
}
=== Perintah ===
gcc -S -O0 -o - sum.c
gcc -S -O2 -o - sum.c
=== Badan fungsi sum pada -O0 ===
sum:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -20(%rbp)
movl $0, -4(%rbp)
movl $1, -8(%rbp)
jmp .L2
.L3:
movl -8(%rbp), %eax
addl %eax, -4(%rbp)
addl $1, -8(%rbp)
.L2:
movl -8(%rbp), %eax
cmpl -20(%rbp), %eax
jle .L3
movl -4(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
=== Badan fungsi sum pada -O2 ===
sum:
.LFB0:
.cfi_startproc
testl %edi, %edi
jle .L4
leal 1(%rdi), %ecx
xorl %edx, %edx
andl $1, %edi
movl $1, %eax
je .L3
movl $2, %eax
movl $1, %edx
cmpl %ecx, %eax
je .L1
.p2align 4
.p2align 4
.p2align 3
.L3:
leal 1(%rdx,%rax,2), %edx
addl $2, %eax
cmpl %ecx, %eax
jne .L3
.L1:
movl %edx, %eax
ret
=== Perbandingan ukuran assembly ===
ukuran assembly -O0 : 814 byte
ukuran assembly -O2 : 758 bytePada -O0, setiap variabel lokal disimpan di slot stack frame, sehingga s berada di -4(%rbp), i di -8(%rbp), dan n di -20(%rbp). Setiap iterasi melakukan tiga akses memori: memuat i ke eax, menambahkan eax ke s di memori, dan menambahkan 1 ke i di memori. Pola muat-hitung-simpan ini persis sama dengan pola yang dijelaskan pada topik pengenalan target native, sebab GCC pada -O0 mematikan seluruh pass optimasi sehingga pemetaan antara sumber dan assembly tetap dekat. Pada -O2, GCC menghapus akses memori untuk variabel lokal, mengganti perulangan dengan operasi register, dan melakukan vektorisasi loop parsial pada instruksi leal 1(%rdx,%rax,2), %edx yang menggabungkan dua iterasi menjadi satu. Akibatnya, ukuran assembly turun dari 814 byte menjadi 758 byte, dan kecepatan eksekusi naik karena akses memori pada badan loop berkurang drastis.
Clang sebagai front end LLVM#
Clang adalah front end C, C++, dan Objective-C yang menargetkan LLVM IR alih-alih assembly langsung. Struktur Clang mengikuti pola front end yang sama dengan GCC, yaitu lexer, parser, analisis semantik, lalu codegen ke IR, tetapi keluarannya bukan GIMPLE melainkan LLVM IR. Keuntungan menargetkan LLVM adalah front end hanya perlu menghasilkan IR, sedangkan seluruh pipeline optimasi dan back end menjadi tanggung jawab ekosistem LLVM. Clang dan LLVM dikembangkan sebagai proyek yang saling lepas, sehingga front end bahasa baru seperti Rust pernah memakai LLVM sebagai back end tanpa menulis ulang pass optimasi.
Karena Clang tidak terpasang pada sandbox ini, demo pembangkitan LLVM IR memakai llvmlite, yaitu binding Python ke LLVM yang sudah tersedia secara default pada sandbox. llvmlite membangun IR secara terprogram, sehingga keluarannya memiliki bentuk yang sama dengan IR yang akan dihasilkan Clang untuk fungsi yang setara. Berkas clang_demo.py berisi kode yang membangun modul LLVM untuk fungsi add(a, b) = a + b, lalu mencetak IR-nya.
"""Demo pembangkitan LLVM IR sebagai alternatif Clang yang tidak tersedia.
Clang adalah front end C dan C++ yang menargetkan LLVM IR. Karena Clang
tidak terpasang pada sandbox ini, demo memakai pustaka `llvmlite` (binding
Python ke LLVM) untuk membangun LLVM IR secara terprogram. Keluaran IR
yang dihasilkan sama bentuknya dengan IR yang akan dihasilkan Clang untuk
fungsi `add(a, b) = a + b` yang setara.
Pemakaian:
python3 clang_demo.py
"""
import sys
# llvmlite adalah binding Python ke LLVM, terpasang default pada sandbox
from llvmlite import ir
def build_add_module() -> ir.Module:
"""Bangun modul LLVM yang berisi fungsi `add(a, b) = a + b`."""
module = ir.Module(name="add_module")
# Tipe fungsi: int add(int a, int b)
fnty = ir.FunctionType(ir.IntType(32), [ir.IntType(32), ir.IntType(32)])
func = ir.Function(module, fnty, name="add")
func.args[0].name = "a"
func.args[1].name = "b"
# Blok masuk
block = func.append_basic_block(name="entry")
builder = ir.IRBuilder(block)
# a + b
result = builder.add(func.args[0], func.args[1], name="result")
builder.ret(result)
return module
def main() -> None:
# Verifikasi llvmlite terpasang dan dapat dipakai
print("=== Pustaka ===")
print("llvmlite: binding Python ke LLVM untuk pembangkitan IR terprogram")
print()
module = build_add_module()
print("=== LLVM IR untuk fungsi add(a, b) = a + b ===")
print(str(module))
print("=== Bandingkan dengan Clang ===")
print("Perintah yang setara bila Clang tersedia:")
print(" clang -S -O2 -emit-llvm -o - add.c")
print("Keluaran IR di atas memiliki bentuk yang sama, sebab Clang dan")
print("llvmlite sama-sama membangun LLVM IR sebagai representasi antara.")
if __name__ == "__main__":
main()Keluaran berikut diperoleh dengan menjalankan python3 clang_demo.py pada sandbox. IR yang dihasilkan memakai register virtual bernama %a, %b, dan %result dengan tipe i32, sebagaimana yang akan dihasilkan Clang untuk fungsi C int add(int a, int b) { return a + b; }.
=== Pustaka ===
llvmlite: binding Python ke LLVM untuk pembangkitan IR terprogram
=== LLVM IR untuk fungsi add(a, b) = a + b ===
; ModuleID = "add_module"
target triple = "unknown-unknown-unknown"
target datalayout = ""
define i32 @"add"(i32 %"a", i32 %"b")
{
entry:
%"result" = add i32 %"a", %"b"
ret i32 %"result"
}
=== Bandingkan dengan Clang ===
Perintah yang setara bila Clang tersedia:
clang -S -O2 -emit-llvm -o - add.c
Keluaran IR di atas memiliki bentuk yang sama, sebab Clang dan
llvmlite sama-sama membangun LLVM IR sebagai representasi antara.Bila mesin pembaca memasang Clang, perintah clang -S -O2 -emit-llvm -o - add.c pada berkas C yang berisi fungsi add akan menghasilkan IR dengan bentuk yang sama, lengkap dengan blok entry dan instruksi add i32 %a, %b. Posisi LLVM sebagai target codegen dibahas tuntas pada topik Posisi LLVM. Untuk keperluan topik ini, cukup dicatat bahwa Clang dan GCC berbagi pola yang sama, yaitu front end menghasilkan IR, lalu back end menurunkan IR menjadi kode native, dengan perbedaan utama pada IR yang ditargetkan dan ekosistem pass yang tersedia.
JIT sebagai penerjemah saat runtime#
JIT adalah penerjemah yang mengompilasi kode sumber atau bytecode menjadi kode mesin pada saat program berjalan, bukan sebelum program dijalankan. Pada spektrum penerjemah yang sudah diperkenalkan pada topik taksonomi, JIT berdiri di antara interpreter dan compiler AOT: keluarannya adalah kode mesin di memori, tetapi proses terjemahan terjadi saat runtime dan hanya untuk kode yang sering dipakai. Tiga implementasi JIT yang sering disebut adalah V8 untuk JavaScript, HotSpot untuk Java, dan PyPy untuk Python.
V8 mengompilasi fungsi JavaScript yang sering dipakai menjadi kode mesin pada saat runtime, dengan profil eksekusi yang mengumpulkan jenis argumen untuk memandu optimasi. HotSpot pada JVM mengawali eksekusi dengan interpreter bytecode, lalu mengompilasi method yang sering dipakai menjadi kode native melalui dua tier, yaitu C1 untuk optimasi ringan dan C2 untuk optimasi berat berbasis profil. PyPy mengompilasi bytecode Python menjadi kode mesin melalui JIT yang dibangun di atas RPython, sehingga program Python yang sering dieksekusi dapat berjalan lebih cepat daripada pada CPython. Ketiga implementasi membagi pola yang sama, yaitu mengawali eksekusi dengan cepat melalui interpreter, lalu mengompilasi kode panas menjadi kode native tanpa menghentikan program.
Kesalahan umum#
Kesalahan konseptual yang paling sering muncul adalah menyamakan GCC dengan satu pass codegen. GCC menjalankan puluhan pass pada IR GIMPLE, SSA, dan RTL, sehingga menyamakannya dengan codegen satu pass pada proyek kursus menyesatkan. Kesalahan kedua adalah menganggap -O2 selalu menghasilkan assembly yang lebih pendek daripada -O0. Demo di atas memperlihatkan bahwa ukuran byte memang turun, tetapi jumlah baris bisa naik karena pass seperti vektorisasi menambah instruksi padat pada baris-baris pendek. Kesalahan ketiga adalah menyamakan JIT dengan compiler AOT yang lebih cepat, padahal JIT mengompilasi saat runtime dan membayar biaya kompilasi selama eksekusi program.
Kesalahan keempat adalah menganggap Clang dan GCC berbeda secara mendasar dari sudut pandang pipeline. Keduanya mengikuti pola front end, IR, dan back end yang sama, dengan perbedaan utama pada IR yang ditargetkan dan ekosistem pass. Pemahaman pola yang sama menyederhanakan perbandingan: bagian yang dipelajari pada proyek kursus, yaitu lexing, parsing, analisis semantik, IR, dan codegen, adalah bagian yang sama yang dijalankan GCC dan Clang pada skala produksi.
Ringkasan#
- GCC menjalankan pipeline multi-pass dengan IR GIMPLE, SSA, dan RTL, sehingga tingkat
-O0hingga-O3menetapkan kombinasi pass yang dijalankan, bukan satu pass codegen. - Demo
-O0versus-O2memperlihatkan transformasi dari pola muat-hitung-simpan yang mudah dipetakan ke sumber menjadi kode register padat dengan vektorisasi loop. - Clang adalah front end C dan C++ yang menargetkan LLVM IR, dengan pola pipeline yang sama dengan GCC tetapi ekosistem pass yang berbeda.
- JIT mengompilasi saat runtime dan hanya untuk kode yang sering dipakai, sehingga berdiri di antara interpreter dan compiler AOT, dengan V8, HotSpot, dan PyPy sebagai contoh kanonik.
Referensi#
- GCC Manual — Options That Control Optimization — GNU Project
— Penjelasan resmi tingkat
-O0hingga-O3beserta pass yang dinyalakan tiap tingkat. - GCC Internals — GNU Project — Dokumentasi struktur internal GCC, termasuk GIMPLE, SSA, dan RTL sebagai IR bertingkat.
- Clang — a C language family frontend for LLVM — LLVM Project — Halaman utama Clang dengan tautan ke dokumentasi front end dan integrasi LLVM.
- Just-in-time compilation — Wikipedia — Penjelasan konseptual JIT, termasuk perbandingannya terhadap compiler AOT dan interpreter.
- V8 JavaScript engine — V8 Developers — Dokumentasi V8 yang menjelaskan pipeline interpreter Ignition dan compiler TurboFan sebagai JIT produksi.