You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Visitor menang di DeepSeq dan Repetition, imbang di skenario lain. HotSpot ng-inline accept() → visitXxx() sampai identik kayak virtual dispatch — bahkan bisa lebih cepet karena visitor method gak perlu interface dispatch (concrete method call langsung).
3.2 Android (ART)
Skenario
virtual
sealed
tagged
visitor
DeepSeq (100)
1,475ns
1,669ns
1,594ns
1,439ns
HeavyChoice (50)
175ns
189ns
203ns
113ns
NestedChoice (20)
132ns
135ns
150ns
123ns
Repetition (500x)
7,162ns
7,373ns
7,462ns
7,123ns
SimpleLiteral
27ns
28ns
27ns
22ns
Realistic
327ns
315ns
303ns
310ns
🏆 Android: visitor dominates — menang 5/6 skenario. Tagged cuma menang tipis di Realistic (303ns vs 310ns). ART nge-JIT accept() → visitXxx() chain jadi near-native, mirip polanya sama HotSpot.
3.3 JVM vs Android Head-to-Head (visitor)
Skenario
JVM (visitor)
Android (visitor)
JVM × faster
DeepSeq (100)
388ns
1,439ns
3.7×
HeavyChoice (50)
78ns
113ns
1.4×
NestedChoice (20)
69ns
123ns
1.8×
SimpleLiteral
11ns
22ns
2.0×
Realistic
76ns
310ns
4.1×
Visitor menutup gap JVM vs Android: dari 9× (virtual) jadi 2-4× (visitor). ART lebih optimal di visitor pattern dibanding virtual dispatch.
4. Analisis
Kenapa JVM virtual & visitor sama cepatnya?
HotSpot punya inline caching + CHA (Class Hierarchy Analysis). Di monomorphic call site, HotSpot:
Profile: "oh, expr selalu VLit"
Guard: tambahin type check (if (expr.getClass() != VLit.class) deopt)
Inline: method body langsung di-inline ke call site
Untuk visitor: accept() dipanggil dengan MatchVisitor terus-menerus → HotSpot sadar ini monomorphic → inline accept() → sadar visitLit() juga monomorphic → inline juga. Hasilnya 2 layer method call jadi 0 overhead.
Kenapa sealed kalah?
instanceof Sealed$SLit harus traverse sealed class hierarchy. HotSpot gak bisa nge-inline ini jadi branchless code. Tetep harus check type tag di header object. Ini mirip kayak switch — ada branching cost.
Kenapa tagged paling bawah?
Switch int paling "jujur" — gak ada yang bisa di-inline. HotSpot meng-compile tableswitch jadi jump table, tapi tetep ada branch + table lookup overhead.
Sequence 100 level, semua match — minim backtracking
HeavyChoice (50)
Choice 50 cabang, cabang terakhir yang match — backtracking berat
NestedChoice (20)
Choice bersarang 20 level dalam — rekursi dalam + backtracking
Repetition (500x)
(ab)* di string ab × 500 — loop 500 iterasi
SimpleLiteral
Literal "hello" — leaf node, gak ada rekursi
Realistic
(if|while)([a-dxy]+) — grammar campuran 5 tipe node
5. Hasil Benchmark (Debug)
Skenario
Poly
Switch Expr
Switch *Expr
Tag
TagCopy
Visitor
DeepSeq (100)
2,789ns
2,895ns
3,868ns
2,974ns
3,141ns
2,960ns
HeavyChoice (50)
149ns
175ns
699ns
159ns
262ns
201ns
NestedChoice (20)
94ns
108ns
508ns
105ns
214ns
328ns
Repetition (500x)
13,821ns
14,109ns
14,236ns
14,094ns
14,631ns
17,743ns
SimpleLiteral
26ns
24ns
23ns
24ns
24ns
23ns
Realistic
457ns
467ns
518ns
470ns
491ns
477ns
Tag vs TagCopy Head-to-Head (Debug)
Skenario
Tag (pointer)
TagCopy (value)
Pelambatan
DeepSeq (100)
2,974ns
3,141ns
+5.6%
HeavyChoice (50)
159ns
262ns
+65% 🔥
NestedChoice (20)
105ns
214ns
+104% 🔥🔥
Repetition (500x)
14,094ns
14,631ns
+3.8%
SimpleLiteral
24ns
24ns
0%
Realistic
470ns
491ns
+4.5%
6. Ranking Akhir
🥇 Polymorphism — itable dispatch, O(1), gak ada boxing ulang
🥈 Tagged Union — switch int murni, pass pointer, nyaris setara Poly
🥉 Switch (Expr) — type switch pada interface, overhead dikit
4️⃣ Visitor — double dispatch, lumayan buat grammar pipih
5️⃣ TagCopy — copy 80B struct tiap rekursi, lumayan buat tree dangkal
💀 Switch (*Expr) — interface boxing + heap alloc tiap rekursi, brutal
7. Analisis
Kenapa Polymorphism Juara?
Go menggunakan itable (interface table) untuk method dispatch. Ketika kamu panggil e.Match(...), runtime tinggal lookup tabel vtable sekali lalu lompat. Gak ada branching switch, gak ada alokasi tambahan.
Kenapa Tag (pointer) Nempel Ketat?
MatchTag(n *Node, ...) switch pada n.Tag yang bertipe int. Go compiler bisa mengoptimalkan switch int menjadi jump table. Ditambah tipe *Node langsung (tanpa interface boxing), overhead dispatch hampir nol.
TagCopy vs Tag: Kenapa Copy 80 Byte Lumayan Sakit?
Tiap rekursi MatchTagCopy(*sub, ...):
sub = *Node (8 byte pointer)
*sub = deref pointer → copy 80 byte struct ke stack argument function
NestedChoice 20 level = 20 × 80 byte = 1.6KB copy per path — semua di stack sih (gak heap alloc), tapi lumayan
Tetep *jauh lebih baik dari Switch(Expr) yang bikin heap alloc tiap rekursi. TagCopy "cuma" 2× lebih lambat di NestedChoice vs Switch(*Expr) yang 5×.
Kenapa Switch (*Expr) Paling Hancur?
Ini double whammy:
Tiap rekursi MatchSwitchPtr(&sub, ...) — sub adalah Expr (interface), &sub adalah *Expr. Go harus bikin interface baru di heap.
(*e).(type) — dereference pointer dulu, baru type-switch.
Akibatnya di HeavyChoice (50 cabang): 52 alloc vs 2 alloc untuk semua pendekatan lain.
Kenapa SimpleLiteral Semua Sama?
Gak ada rekursi. Compiler Go meng-inline semuanya. Hasilnya identik.
8. Rekomendasi
Use case
Pilih
Performa maksimal, grammar production
Polymorphism
Ingin 1 jenis struct, mudah serialisasi
Tagged Union (pointer)
Operasi terpisah per file, clean architecture
Polymorphism
Ingin ekstensibilitas (tambah operasi tanpa ubah node)
Visitor
Prototype cepet, kode sedikit
Switch (Expr)
JANGAN
*Switch (Expr)
9. Cara Menjalankan
# Test semua pendekatan
go test -v -run Test ./...
# Benchmark semua
go test -bench=. -benchmem -benchtime=1s -count=5 ./...
# Benchmark spesifik
go test -bench=HeavyChoice -benchmem ./...
TagCopy vs Tag ratio stabil di debug maupun release (~2× di NestedChoice, ~68% di HeavyChoice). Overhead copy 80-byte murni dari stack, gak terpengaruh mode build.
Perbandingan Alloc (identik semua)
Skenario
Debug
Release
DeepSeq
9,296 B / 209 alloc
9,296 B / 209 alloc
HeavyChoice
48 B / 2 alloc
48 B / 2 alloc
NestedChoice
48 B / 2 alloc
48 B / 2 alloc
5. Cara Build & Run
# Build release binarycd cmd/bench && go build -ldflags="-s -w" -o peg-bench .# Run
./peg-bench
# Binary size
ls -lh peg-bench
# 1.8MB stripped# Alternative: go test benchmark (debug mode)
go test -bench=. -benchmem -benchtime=1s -count=5 ./...
6. Insight
Debug vs Release selisih <10% — compiler Go udah optimal. Build release lebih cepet dikit dari overhead test runner.
TagCopy overhead murni dari struct copy (stack) — gak ada alloc tambahan. NestedChoice 20 level: 20 × 80 byte = 1.6KB copy. Beda sama Switch(*Expr) yang bikin heap alloc.