1 poin oleh GN⁺ 2024-08-20 | 1 komentar | Bagikan ke WhatsApp
  • Dengan ditambahkannya musttail ke Clang, bahasa keluarga C kini dapat memanfaatkan tail call yang dijamin, dan penerapannya pada parser protobuf mendemonstrasikan performa di atas 2GB/dtk
  • Intinya adalah membuat pemanggilan fungsi lebih mirip jmp daripada call, sehingga penggunaan stack pada pemanggilan beruntun turun dari O(n) menjadi O(1) dan dapat diperlakukan seperti loop iteratif
  • Protobuf wire format harus menginterpretasikan tag/nilai dan bercabang ke field dalam urutan arbitrer, sehingga struktur while + switch tradisional memiliki masalah optimisasi yang mirip dengan dispatch opcode interpreter
  • Parser eksperimental upb menghubungkan fungsi-fungsi parser kecil dengan tail call, alih-alih memakai satu fungsi besar, sehingga pada jalur cepat ia menghindari penggunaan stack, spill register, prolog/epilog
  • Pendekatan ini memiliki batasan: kualitas kode memburuk tajam jika tercampur non-tail call, dan musttail adalah ekstensi non-standar; karena itu, untuk merilis parser cepat secara nyata diperlukan disiplin pemanggilan dan langkah portabilitas

Parsing protobuf berkecepatan tinggi yang dibuka oleh musttail Clang

  • Atribut pernyataan [[clang::musttail]] / __attribute__((musttail)) ditambahkan ke branch main Clang, sehingga C, C++, dan Objective-C dapat memperoleh jaminan tail call
  • Di sini tail call tidak dipakai sebagai teknik pemrograman fungsional, melainkan sebagai alat optimisasi untuk menurunkan biaya branching pada parser dan interpreter
  • Hasil penerapan teknik ini pada parsing protobuf dalam upb pull/310 mendemonstrasikan performa parsing di atas 2GB/dtk
    • Diperkenalkan sebagai hasil yang lebih dari dua kali lebih cepat dibanding tingkat terbaik sebelumnya
    • Karena beberapa teknik berkontribusi bersama, interpretasi bahwa “tail call saja membuatnya 2x lebih cepat” tidak tepat
    • Tail call adalah salah satu elemen kunci yang memungkinkan peningkatan performa ini
  • Perubahan setelahnya dibahas dalam A Tail Calling Interpreter For Python (And Other Updates)

Mengapa tail call berperilaku seperti struktur iteratif

  • Tail call adalah pemanggilan fungsi terakhir yang dilakukan tepat sebelum sebuah fungsi mengembalikan nilai
  • Jika optimisasi tail call diterapkan, compiler menghasilkan instruksi jmp alih-alih call biasa
    • Melewati pembuatan stack frame baru atau penyimpanan return address
    • Caller f() langsung melompat ke callee g()
    • g() langsung kembali ke fungsi yang memanggil f()
  • Berkat sifat ini, tail call dapat menggantikan struktur iteratif
    • Bahkan pada n tail call beruntun, penggunaan stack turun dari O(n) menjadi O(1)
    • Overhead call hilang, sehingga pemanggilan fungsi dapat diperlakukan seperti branch biasa
  • Ide ini bukan hal baru; akarnya dapat ditelusuri hingga makalah Guy Steele tahun 1977 dan “Lambda Papers” antara 1975–1980
  • Clang sebelumnya sudah dapat mengoptimalkan tail call pada build optimisasi seperti -O2, tetapi perilaku lama lebih mendekati best-effort
    • Pada build tanpa optimisasi, besar kemungkinan ia dikompilasi menjadi call sungguhan
    • Agar tail call aman digunakan sebagai struktur iteratif, optimisasi harus dijamin di semua mode build
    • musttail menyediakan jaminan ini

Bottleneck yang sama pada loop interpreter dan parser protobuf

  • Mike Pall dari LuaJIT menulis interpreter LuaJIT 2.x dalam assembly, bukan C, dan menganggapnya sebagai salah satu alasan utama interpreter tersebut cepat
  • Compiler C menghadapi dua masalah khususnya pada loop utama interpreter
    • Semakin besar fungsi dan semakin kompleks control flow, semakin sulit bagi register allocator untuk mempertahankan data penting di register
    • Jika jalur cepat dan jalur lambat bercampur dalam fungsi yang sama, jalur lambat ikut menurunkan kualitas kode jalur cepat
  • Protobuf wire format juga memiliki struktur yang mirip interpreter
    • Wire format adalah rangkaian pasangan tag/nilai
    • Tag memuat nomor field dan wire type
    • Tag berperilaku mirip opcode yang memberi tahu cara mem-parse data field tersebut
    • Nomor field dapat muncul dalam urutan arbitrer, sehingga kode harus siap melakukan dispatch ke bagian mana pun
  • Parser protobuf tradisional biasanya memakai struktur switch di dalam loop while, dan selama sebagian besar masa keberadaan protobuf, pendekatan ini digunakan sebagai tingkat terbaik
  • Dalam parsing nyata, pengecualian seperti ketidakcocokan wire type, data rusak, atau mencapai akhir buffer dapat terjadi hampir di setiap tahap
    • Jalur cepat harus dijaga sesingkat dan sestabil mungkin
    • Untuk kasus sulit, diperlukan kode fallback yang lebih besar dan kompleks, dan terkadang juga melakukan pemanggilan fungsi out-of-line

Desain parser upb berbasis tail call

  • Parser eksperimental upb tidak memakai satu fungsi parsing besar, melainkan memisahkan tiap operasi menjadi satu fungsi kecil
  • Tiap fungsi memanggil operasi berikutnya dengan tail call
    • Berkat calling convention x86-64, argumen parsing bersama diteruskan melalui register
    • Semua fungsi parsing memakai set argumen yang sama, sehingga perpindahan nilai antar-panggilan berkurang
  • Fungsi parser field fixed-width 4 byte pada contoh bekerja dengan alur berikut
    • Mendekode informasi field dari data
    • Jika wire type tidak cocok, melakukan MUSTTAIL return ke fallback()
    • Melewati tag dan menyimpan data ke pesan
    • Setelah membaca tag berikutnya, melakukan tail call ke dispatch() yang bercabang ke parser field yang sesuai
  • Assembly yang dihasilkan Clang tidak memiliki prolog, epilog, spill register, maupun penggunaan stack pada jalur cepat
    • Titik keluar hanya berupa jmp ke fallback atau dispatch
    • Karena argumen sudah berada di register yang benar, tidak diperlukan kode penerusan parameter tambahan
  • Struktur ini secara konseptual memandang loop interpreter besar sebagai satu fungsi kompleks, tetapi implementasi nyatanya dipecah menjadi fungsi-fungsi tingkat basic block dan meneruskan control flow lewat tail call
  • Jika jalur cepat dan jalur lambat dipisahkan ke fungsi berbeda, kemungkinan perubahan kode fallback menggoyahkan kualitas kode jalur cepat menjadi lebih kecil
    • Jika perlu, inlining dapat dicegah dengan noinline
    • Urutan assembly pada jalur cepat pada praktiknya dapat dipatok

Kualitas code generation C yang terlihat pada contoh LuaJIT

  • Jika pola yang sama diterapkan pada contoh LuaJIT, hasil yang mendekati assembly tulisan tangan dapat diperoleh dari kode C
  • Fungsi contoh ADDVN melakukan operasi berikut
    • Mengekstrak register dan indeks konstanta dari instruksi
    • Jika type check gagal, pindah ke fallback
    • Menambahkan konstanta ke nilai register
    • Membaca opcode berikutnya dan melakukan tail call ke fungsi dalam tabel opcode
  • Pada assembly yang dihasilkan, perbaikan yang tersisa relatif kecil
    • Ada jmp terpisah setelah conditional branch
    • Alih-alih jmp qword ptr [rsi + 8*rax], ia memuat ke rax lalu memakai jmp rax
  • Bagian seperti ini diperlakukan sebagai isu code generation kecil yang dapat diperbaiki di Clang

Batasan berupa non-tail call dan portabilitas

  • Hal paling penting yang perlu diperhatikan dalam pendekatan ini adalah kualitas assembly memburuk tajam jika ada non-tail call di dalam fungsi
    • Satu non-tail call memaksa pembuatan stack frame
    • Banyak data dapat di-spill ke stack
  • Untuk menghindarinya, diperlukan disiplin untuk meng-inline pemanggilan fungsi lain atau hanya melakukannya sebagai tail call
  • Pada parsing protobuf, penanganan varint adalah salah satu kesulitan utama
    • Kasus umum dan cepat adalah varint 1 byte
    • Varint yang lebih panjang bukanlah error, tetapi kasus yang jarang
    • Jika penanganan pengecualian ini di-inline, kualitas kode jalur cepat dapat memburuk
    • Jika melakukan tail call ke fungsi fallback, setelah penanganan selesai sulit untuk melanjutkan operasi semula, sehingga fallback harus menangani operasi sampai tuntas
    • Akibatnya muncul duplikasi kode dan kompleksitas
  • Pembaruan 2025-01-27 menambahkan cara untuk meredakan masalah ini lewat calling convention
    • __attribute__((preserve_most)) adalah calling convention yang dapat digunakan untuk fungsi fallback; ia memindahkan tanggung jawab mempertahankan hampir semua register ke callee sehingga biaya spill bergeser ke sisi fallback
    • Bug crash Clang terkait atribut ini diperbaiki pada 2023
    • __attribute__((preserve_none)) adalah calling convention yang dapat digunakan untuk fungsi tail calling; ia menghilangkan beban mempertahankan register dan memakai lebih banyak register untuk argumen
    • Di antara kedua cara tersebut, preserve_none dinilai sebagai pilihan yang lebih baik karena kurang intrusif
  • Batasan lain adalah musttail merupakan ekstensi compiler non-standar
    • Diharapkan menyebar ke GCC, Visual C++, dan lainnya serta distandarkan, tetapi itu bukan hal yang dekat
    • Tanpa musttail, setidaknya satu return sungguhan diperlukan untuk setiap iterasi loop konseptual
    • Fallback ini belum diimplementasikan di upb, dan diperkirakan diperlukan makro yang melakukan tail call ke dispatch atau sekadar return, bergantung pada ketersediaan musttail

Status penerapan upb dan kemungkinan perluasan

  • Parser di atas 2GB/dtk diajukan ke upb, sebuah library protobuf kecil yang ditulis dalam C
  • Kode tersebut berfungsi sepenuhnya dan lulus semua protobuf conformance test, tetapi pada saat penulisan belum di-rollout ke mana pun
  • Desain ini belum diimplementasikan pada protobuf versi C++
  • Setelah itu, upb diperbarui agar menggunakan musttail, sehingga satu hambatan besar untuk memproduksikan parser cepat dihapus
  • Teknik yang sama juga dapat memberi keuntungan performa besar bagi interpreter bahasa utama yang ditulis dalam C, seperti Python, Ruby, PHP, dan Lua

1 komentar

 
GN⁺ 2024-08-20
Pendapat di Hacker News
  • Ada sintaks dalam proposal standar C untuk tail call, bentuknya return goto (expression);
    Hal yang saya sukai dibanding [[musttail]] standar adalah adanya jaminan bahwa masa hidup objek lokal berakhir. Jadi ini bisa diimplementasikan tanpa escape analysis yang luas
    [0] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3266.htm#5...

    • Saya penasaran kenapa return goto lebih mudah diimplementasikan. [[musttail]] juga, sekilas, tampaknya mengakhiri masa hidup objek lokal
      Setelah saya baca cepat, tertulis bahwa fungsi yang dipanggil pada posisi tail harus memiliki tipe yang sama dengan target panggilan. Ini adalah syarat untuk memastikan tidak perlu konversi nilai balik, dan agar ruang penerusan argumen serta calling convention tetap terjaga
      Keluhan yang sering saya lihat tentang [[musttail]] yang saya implementasikan di Clang adalah bahwa batasan ini terlalu ketat tanpa perlu. Beberapa arsitektur mengizinkan tail call meski tipenya tidak sepenuhnya cocok: https://github.com/llvm/llvm-project/issues/54964
      Memang benar kalau dikatakan “kalau begitu kodenya jadi tidak portabel”, tetapi optimisasi tail call itu sendiri pada dasarnya tidak portabel. Misalnya, beberapa target secara fundamental tidak mendukung optimisasi tail call, seperti WASM yang tidak memiliki ekstensi tail call
    • Menguatnya kembali dorongan untuk menambahkan fitur baru ke C terasa menjanjikan sekaligus cukup mengkhawatirkan
      Saya antusias karena ada perubahan dan tambahan yang memang perlu masuk, bahkan ide-ide yang perlu diperjelas, tetapi siklus pembaruan C++ yang agresif pada akhirnya terasa seperti menambal benjolan di atas benjolan lain
      Masalahnya terutama ketika fitur-fitur saling berinteraksi buruk jauh lebih awal dari yang diperkirakan. Saya berharap proses standardisasi tidak hanya bersandar pada dokumen pembenaran, tetapi memilih dengan sangat konservatif sambil menguji fitur secara memadai pada codebase yang besar dan beragam
  • Jika tertarik pada sisi Rust, ada RFC lama untuk menambahkan keyword become yang menyediakan optimisasi tail call yang dijamin
    Awalnya ditunda agar fokus pada target edisi 2018, dan keputusan itu tepat, tetapi gagasan ini belakangan ditinjau kembali. Bisa saja kembali lagi
    [0]: https://github.com/rust-lang/rfcs/pull/1888
    [1]: https://github.com/rust-lang/rfcs/pull/3407

  • Cara interpreter biasanya mendapatkan peningkatan kecepatan seperti ini di C++ adalah dengan memakai computed goto. Dengan begitu, tidak ada noise terkait calling convention pada jalur dari satu opcode ke opcode berikutnya
    Alasan utama pendekatan computed goto atau tail call lebih cepat daripada loop switch klasik adalah karena mengurangi beban branch predictor. Secara statis, tiap opcode memiliki satu indirect branch, bukan struktur yang secara statis hanya memiliki satu indirect branch

    • Seperti yang disebutkan di artikel, meski memakai computed goto, control-flow graph fungsi menjadi terlalu rumit sehingga alokasi register untuk variabel yang sering dipakai menjadi rapuh
      Jika tiap fungsi kecil dan menerima variabel penting sebagai argumen, alokasi register menjadi jauh tidak serapuh itu
    • Saya penasaran dengan frasa “secara statis, tiap opcode memiliki satu indirect branch”. Dibandingkan dengan satu indirect branch saja, apa maksudnya, dan bagaimana itu dicapai? Akan bagus kalau bisa dijelaskan sedikit lebih rinci
    • Saya dengar pendekatan ini sudah tidak populer selama beberapa waktu. Katanya branch predictor sudah cukup bagus sehingga ini tidak lagi diperlukan
      Namun saya penasaran apakah pernyataan itu tetap berlaku ketika ukuran interpreter membesar
  • Masalah yang tersisa dalam memakai tail call untuk perpindahan konteks adalah bahwa kita menggunakan fungsi-fungsi yang harus memakai calling convention. Sayangnya, register terbuang untuk memulihkan status saat fungsi berakhir
    Analisis lebih rinci dan alternatif menggunakan compiler perantara ada di blog remake LuaJIT: https://sillycross.github.io/2022/11/22/2022-11-22/

    • Selama beberapa tahun terakhir, saya melihat beberapa bahasa menghapus lapisan JIT lalu memasangnya kembali. Sebagian karena kemampuan programmer dan pelajaran yang dipetik, tetapi sebagian juga karena perubahan generasi CPU
      Seperti semua hal lain dalam ilmu komputer, ketika keseimbangan biaya menurut jenis operasi berubah, algoritma terbaik bisa kembali ke cara yang dipakai 15 atau 20 tahun lalu. Karena itu, pemrograman sering tampak seperti tren. Menghidupkan kembali sesuatu bukan berarti tanpa alasan, tetapi melupakan alasan kenapa dulu itu bukan obat mujarab tetap menjadi masalah
      Jika JIT utama menjadi lebih cepat atau lebih lambat, manfaatnya relatif terhadap biaya eksekusi berubah, dan ambang batas pemicunya juga disesuaikan. Lalu jumlah kode yang berjalan di lapisan lain berubah, dan biaya teramortisasi lapisan itu juga bisa memburuk. Rasanya seperti menyeimbangkan pendulum ganda
      Jika lapisan JIT bisa dibuat cukup cepat dan kasar, interpreter bisa dilewati sama sekali. Dari luar, beban kognitif untuk menyeimbangkan pembukuan antara interpreter dan kira-kira dua JIT terlihat besar, sehingga beberapa bahasa tampaknya menahan interpreter dan memakai JIT yang dioptimalkan untuk waktu kompilasi, bukan kecepatan output
      Saya tidak ingat bahasa apa, tetapi setahu saya setidaknya satu tim akhirnya juga menghapus compiler perantara karena masalah keseimbangan ini. Lebih baik fokus pada dua hal daripada menangani tiga
    • Baru-baru ini Clang memasukkan calling convention baru yang membuat tail call seperti ini jauh lebih murah. Ini menghindari kebutuhan pemanggil untuk mempertahankan sebagian register
      Namanya selalu membuat saya bingung, mungkin preserve_all atau preserve_none. Masalahnya adalah perspektif siapa yang dipakai untuk “preserve”
  • Saya memahami atribut musttail sedang dalam proses ditambahkan ke GCC. Patch-nya sedang ditinjau, dan semantiknya kompatibel dengan Clang

    • Saya penasaran bagaimana nasib atribut preserve_most. Apakah ada kemungkinan sesuatu yang mirip masuk ke GCC? Tanpa ini, panggilan non-tail akan merusak interpreter
    • Ini masalah yang sulit. Banyak ABI tidak bisa melakukan tail call bahkan untuk kasus yang sangat mendasar seperti pemanggilan fungsi eksternal dengan argumen dan tipe return yang cocok
      Clang tampaknya punya heuristik yang mengubah urutan pemanggilan untuk panggilan musttail. Misalnya di i686, ia mengubahnya menjadi panggilan noplt. Hal semacam ini tidak ada di dokumentasi Clang: https://clang.llvm.org/docs/AttributeReference.html#musttail
      Secara realistis, yang mungkin dilakukan adalah compiler mengeluarkan pesan diagnostik ketika tidak bisa menghasilkan tail call. Bagi banyak pengguna, kemungkinan itu sudah cukup. Jaminan tail call seperti di Scheme tampaknya sulit terjadi
    • GNUC punya cukup banyak fitur mirip Scheme, jadi cukup mengejutkan bahwa ia tertinggal dalam fitur ini
  • Dukungan C++ juga disebutkan, tetapi di C++ sepertinya tail call akan sangat sedikit
    Misalnya foo() { auto a = SomeClassWithADestructor(); return bar(); } bukan tail call, karena destruksi a terjadi setelah pemanggilan bar()

    • Jika compiler bisa membuktikan bahwa tidak ada efek samping jarak jauh di antara baris-baris itu, bukankah ia bisa memanggil destructor sebelum menjalankan bar?
      Saya penasaran apakah standar C++ memang mengharuskan destructor dipanggil di akhir blok, atau boleh dipanggil segera setelah variabel tidak lagi digunakan
  • Mungkin contohnya terlalu sederhana, tetapi __attribute__((musttail)) tampaknya tidak benar-benar diperlukan untuk menghasilkan kode yang bagus
    Jika fungsi penanganan error adalah jalur yang jarang dilalui, kecepatan pemanggilannya juga sepertinya tidak terlalu penting
    Struktur seperti if (unlikely(malformed)) return error(); switch (data_type) { case x: return handle_x(); case y: return handle_y(); } tampaknya cukup andal menghasilkan jump table yang bagus

    • Memang compiler sudah sejak lama melakukan eliminasi tail call, tetapi dalam teknik ini “cukup andal dihasilkan” tidak cukup. Itu harus dijamin, atau kompilasi harus gagal
      Kalau tidak, struktur ini tidak akan berjalan dan stack langsung meledak. Inti dari [[musttail]] adalah bahwa eliminasi tail call itu wajib. Compiler tidak punya pilihan lain
    • Seperti yang ditunjukkan disassembly di artikel, alasan jalur fallback menjadi masalah bukan karena seberapa cepat pemanggilan itu sendiri. Keberadaan pemanggilan itu saja bisa membuat compiler membuat stack frame untuk seluruh fungsi dan menumpahkan register ke sana. Dampaknya juga mengenai jalur cepat
      Tentu saja, ungkapan “memaksa” mungkin tidak sepenuhnya tepat. Tidak ada ketentuan bahwa compiler harus memiliki struktur stack frame tunggal untuk semua jalur eksekusi fungsi, dan juga tidak ada ketentuan bahwa fungsi internal linkage atau fungsi namespace anonim yang alamatnya tidak diambil harus memakai ABI standar. Namun semua compiler yang pernah saya lihat, termasuk Clang, pada praktiknya melakukan itu. Jadi kita butuh cara untuk memberi tahu compiler agar tidak mengkhawatirkan ABI dan tidak membuang waktu mempertahankan register antar-pemanggilan
      Jump table tentu saja dibuat dengan baik. Namun jika hasilnya dijalankan melalui sesuatu seperti perf report dan bytecode uji tidak merepresentasikan loop pendek, Anda akan melihat salah satu dari dua hal. Entah ada branch misprediction di setiap dispatch, atau compiler berpikir “sepertinya ini mau memakai interpreter” lalu memindahkan indirect jump ke akhir setiap case. Saya pernah melihat hal seperti ini di Clang. Dalam kedua kasus, alokasi register pada kode yang dihasilkan kemungkinan besar akan cukup buruk
  • Saya penasaran seberapa cepat trampoline, yaitu cara yang mengembalikan fungsi berikutnya sebagai function pointer lalu memanggilnya dari loop luar. Keuntungannya adalah ini C yang portabel

    • C sering dipakai sebagai bahasa target untuk compiler bahasa tingkat tinggi
      Bahasa pemrograman Scheme mengharuskan semua tail call tidak menambah stack. Karena itu, para implementor telah mengeksplorasi berbagai teknik, termasuk trampoline
      Saya tidak punya referensi untuk dikutip, tetapi jawabannya bisa ditemukan dalam makalah-makalah tentang kompilasi Scheme ke C. Jika bahasa target tidak punya jaminan optimisasi tail call, program yang dihasilkan akan menjadi lambat
      Selain itu, inilah alasan khususnya para implementor bahasa tingkat tinggi tidak senang ketika optimisasi tail call dihapus dari spesifikasi JavaScript. Ada juga solusi yang mempertahankan optimisasi tail call sekaligus stack inspection
      https://github.com/schemedoc/bibliography/blob/master/page8....
    • Saya pikir alasan optimisasi tail call cepat adalah karena loop yang dihasilkan dapat diprediksi, sehingga instruction prefetch CPU dan prefetch memori bekerja dengan baik
      Jika melompat lewat function pointer, kemungkinan itu tidak bisa diprediksi sejauh itu, dan sulit mendapatkan keuntungan yang sama
      Tentu saja harus diukur, dan saya sendiri belum mencobanya
  • Saya pernah menulis decoder/encoder Protobuf dan parser IML dalam C, serta binding Python, dan ada beberapa hal yang ingin saya katakan tentang pengukuran kecepatan parsing
    Jika pustaka ini hanya disediakan sebagai binding untuk bahasa terkelola, ada variabel tambahan yang bisa mengungguli semua hal lain dari sisi performa. Saya tidak tahu soal Ruby atau PHP, tetapi di Python saya melihat peningkatan kecepatan yang drastis ketika tidak memakai enumerator. Jika enumerator Protobuf dikonversi menjadi enumerator Python, keuntungan apa pun yang bisa diperoleh dari kode C akan tertindih oleh waktu pembuatan berbagai objek Python. Perbedaannya bisa mencapai beberapa digit orde besaran. Lebih jauh lagi, semua struktur data pendukung juga bisa diimplementasikan dalam C dan hanya mengekspos antarmuka minimal ke Python. Sulit menjawab seberapa adil perbandingan seperti ini dengan kode yang memakai struktur bawaan Python
    Parser Protobuf milik Google untuk Python mungkin masih bisa “lebih cepat” daripada 2GB/s lebih. Alasannya, ia tidak mem-parsing apa pun selain pesan tingkat teratas. Struktur internal pesan baru di-parse saat diperlukan. Jika kode langsung membaca seluruh hasil parsing, kemungkinan besar ia lebih lambat dari 2GB/s, tetapi persoalannya adalah bagaimana membandingkan kedua pendekatan ini secara praktis. Tidak ada jawaban yang jelas karena hasil nyata bergantung pada sifat aplikasinya
    Dalam kasus umum, parsing Protobuf tidak bisa dilakukan secara streaming karena pemrosesan duplikat. Dalam praktiknya, kode yang mem-parsing isi Protobuf akan terkena bottleneck I/O, karena harus menunggu akhir pesan sebelum mulai mem-parsing. Secara terpisah, tergantung pada pesan Protobuf yang tipikal dalam aplikasi, parsing mungkin bisa diparalelkan, dan dengan begitu kemungkinan besar dapat mengungguli parser single-thread. Namun seperti contoh-contoh sebelumnya, ini tidak bisa disebut strategi yang umumnya menang
    Biasanya jauh lebih efisien untuk menggabungkan parsing dengan pembuatan objek domain. Aplikasi hampir selalu harus melalui tahap ini. Bagaimana fitur ini bisa diakses dari parser sering kali menentukan parser mana yang akan menang
    Kesimpulannya, Protobuf, dan mungkin parser secara umum, bukanlah target yang baik untuk pengukuran dan perbandingan kecepatan. Ia terlalu low-level dan desainnya juga kurang baik, sehingga sulit dijadikan tolok ukur benchmark performa

    • Saya tidak memahami bagian “dalam kasus umum, parsing Protobuf tidak bisa dilakukan secara streaming karena pemrosesan duplikat”
      Akan bagus jika dijelaskan lebih rinci bagaimana aturan bahwa field terakhir yang menang menghalangi parsing streaming
  • GCC dan Clang sudah lama memiliki opsi -foptimize-sibling-calls, sehingga tail call bisa didapat bahkan pada build debug
    Tentu saja, menjadi peningkatan besar jika fitur ini distandardisasi, dijamin, dan bisa dikontrol pada level fungsi
    [1] https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
    [2] https://clang.llvm.org/docs/ClangCommandLineReference.html#t...