1 पॉइंट द्वारा GN⁺ 2024-08-20 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Clang में musttail जुड़ने से C-परिवार की भाषाओं में guaranteed tail call का उपयोग संभव हुआ, और इसे protobuf parser पर लागू करके 2GB/s से अधिक प्रदर्शन दिखाया गया
  • मुख्य विचार यह है कि function call को call के बजाय jmp के ज्यादा करीब बनाया जाए, ताकि लगातार calls में stack उपयोग O(n) से O(1) तक घटे और उसे loop की तरह संभाला जा सके
  • protobuf wire format में tag/value को interpret करते हुए arbitrary order के fields पर branch करना पड़ता है, इसलिए पारंपरिक while + switch संरचना को interpreter opcode dispatch जैसी optimization समस्याएँ झेलनी पड़ती हैं
  • upb के experimental parser में एक बड़े function के बजाय छोटे parser functions को tail call से जोड़ा गया, जिससे fast path में stack usage, register spill, prologue/epilogue से बचा जा सके
  • इस तरीके में अगर non-tail call मिल जाए तो code quality काफी खराब हो जाती है, और musttail एक non-standard extension है, इसलिए fast parser को वास्तव में deploy करने के लिए calling convention और portability की रणनीति चाहिए

Clang musttail से संभव हुई high-speed protobuf parsing

  • Clang main branch में [[clang::musttail]] / __attribute__((musttail)) statement attribute जोड़ा गया, जिससे C, C++, Objective-C में tail call guarantee मिल सकती है
  • यहाँ tail call का उपयोग functional programming technique के रूप में नहीं, बल्कि parser और interpreter में branch cost कम करने वाले optimization tool के रूप में किया गया है
  • protobuf parsing में इस technique को लागू करने पर upb pull/310 में 2GB/s से अधिक parsing performance दिखाई गई
    • इसे पहले के सर्वोत्तम स्तर से दोगुने से भी अधिक तेज बताया गया
    • कई techniques ने साथ मिलकर योगदान दिया, इसलिए यह कहना सही नहीं कि “सिर्फ tail call से 2 गुना तेजी मिली”
    • tail call इस performance gain को संभव बनाने वाले मुख्य तत्वों में से एक था
  • बाद के बदलाव A Tail Calling Interpreter For Python (And Other Updates) में दिए गए हैं

Tail call loop की तरह क्यों काम करता है

  • tail call वह आखिरी function call है जो function के return होने से ठीक पहले किया जाता है
  • जब tail call optimization लागू होती है, तो compiler सामान्य call की जगह jmp instruction बनाता है
    • नया stack frame बनाना या return address save करना छोड़ दिया जाता है
    • caller f() सीधे callee g() पर jump करता है
    • g() फिर सीधे उस function को return करता है जिसने f() को call किया था
  • इसी गुण की वजह से tail call loop जैसी संरचना का विकल्प बन सकता है
    • लगातार n tail calls में भी stack usage O(n) से O(1) तक घट जाती है
    • call overhead हटने से function call को सामान्य branch की तरह लिया जा सकता है
  • यह विचार नया नहीं है; इसकी पृष्ठभूमि Guy Steele के 1977 के paper और 1975~1980 के “Lambda Papers” तक जाती है
  • Clang पहले से -O2 जैसी optimized build में tail call optimize कर सकता था, लेकिन पुराना व्यवहार best-effort के करीब था
    • non-optimized build में इसके वास्तविक call के रूप में compile होने की संभावना अधिक थी
    • tail call को loop संरचना की तरह सुरक्षित रूप से इस्तेमाल करने के लिए सभी build modes में optimization की guarantee चाहिए
    • musttail यही guarantee देता है

Interpreter loop और protobuf parser की समान bottleneck समस्या

  • LuaJIT के Mike Pall ने LuaJIT 2.x interpreter को C के बजाय assembly में लिखा था, और इसे fast interpreter का एक बड़ा कारण माना
  • C compiler को interpreter main loop में खास तौर पर दो समस्याएँ आती हैं
    • function जितना बड़ा और control flow जितना जटिल होगा, register allocator के लिए महत्वपूर्ण data को register में बनाए रखना उतना कठिन होगा
    • जब fast path और slow path एक ही function में मिले हों, तो slow path fast path की code quality भी गिरा देता है
  • protobuf wire format की संरचना भी interpreter जैसी है
    • wire format, tag/value pairs की एक श्रृंखला है
    • tag में field number और wire type शामिल होते हैं
    • tag उस opcode जैसा काम करता है जो बताता है कि संबंधित field data को कैसे parse करना है
    • field number arbitrary order में आ सकते हैं, इसलिए code को किसी भी हिस्से में dispatch करने की तैयारी रखनी पड़ती है
  • पारंपरिक protobuf parser आमतौर पर while loop के अंदर switch रखते थे, और protobuf के अधिकांश इतिहास में यही सर्वोत्तम तरीका माना गया
  • वास्तविक parsing में wire type mismatch, corrupted data, buffer end तक पहुँचना जैसी exceptions लगभग हर चरण में हो सकती हैं
    • fast path को जितना संभव हो उतना छोटा और स्थिर रखना चाहिए
    • कठिन मामलों के लिए बड़ा और जटिल fallback code चाहिए, और कभी-कभी out-of-line function call भी करने पड़ते हैं

Tail call आधारित upb parser design

  • upb का experimental parser एक बड़ा parsing function रखने के बजाय हर operation को एक छोटे function में बाँटता है
  • हर function अगले operation को tail call से बुलाता है
    • x86-64 calling convention की वजह से common parsing arguments registers के जरिए पास होते हैं
    • सभी parsing functions एक ही argument set का उपयोग करते हैं, जिससे calls के बीच value movement कम होता है
  • उदाहरण में 4-byte fixed-width field parser function का flow इस तरह है
    • data से field information decode की जाती है
    • wire type मेल न खाने पर fallback() को MUSTTAIL return किया जाता है
    • tag को skip करके data को message में store किया जाता है
    • अगला tag पढ़ने के बाद सही field parser पर branch करने के लिए dispatch() को tail call किया जाता है
  • Clang द्वारा generated assembly में fast path पर prologue, epilogue, register spill, stack usage नहीं होता
    • exit points सिर्फ fallback या dispatch की ओर जाने वाले jmp हैं
    • arguments पहले से सही registers में होते हैं, इसलिए अलग parameter passing code भी नहीं चाहिए
  • इस संरचना में बड़े interpreter loop को वैचारिक रूप से एक जटिल function माना जाता है, लेकिन implementation में उसे basic-block स्तर के functions में बाँटकर tail call से control flow आगे बढ़ाया जाता है
  • fast path और slow path को अलग functions में बाँटने से fallback code में बदलाव के कारण fast path की code quality बिगड़ने की संभावना घटती है
    • जरूरत पड़ने पर noinline से inlining रोकी जा सकती है
    • fast path की assembly sequence को लगभग स्थिर रखा जा सकता है

LuaJIT उदाहरण में दिखने वाली C code generation quality

  • LuaJIT उदाहरण पर भी यही pattern लागू करने से, hand-written assembly के करीब परिणाम C code से मिल सकते हैं
  • उदाहरण ADDVN function ये operations करता है
    • instruction से register और constant index निकाले जाते हैं
    • type check fail होने पर fallback पर जाया जाता है
    • register value में constant जोड़ा जाता है
    • अगला opcode पढ़कर opcode table के function को tail call किया जाता है
  • generated assembly में बचे हुए सुधार अपेक्षाकृत छोटे हैं
    • conditional branch के बाद अलग jmp बनता है
    • jmp qword ptr [rsi + 8*rax] के बजाय rax में load करने के बाद jmp rax इस्तेमाल होता है
  • ऐसे हिस्सों को Clang में सुधारे जा सकने वाले छोटे code generation issues के रूप में देखा गया

non-tail call और portability की सीमाएँ

  • इस तरीके में सबसे बड़ा सावधानी-बिंदु यह है कि अगर function के अंदर non-tail call आ जाए तो assembly quality काफी खराब हो जाती है
    • एक non-tail call भी stack frame बनवाने के लिए मजबूर कर देता है
    • बहुत-सा data stack पर spill हो सकता है
  • इससे बचने के लिए या तो दूसरे function calls को inline करना होगा या सिर्फ tail call के रूप में करना होगा
  • protobuf parsing में varint handling एक प्रमुख कठिनाई है
    • सामान्य और तेज मामला 1-byte varint का होता है
    • लंबा varint error नहीं है, लेकिन अपेक्षाकृत कम सामान्य मामला है
    • अगर इस exception handling को inline किया जाए तो fast path की code quality बिगड़ सकती है
    • अगर fallback function को tail call किया जाए, तो processing के बाद मूल operation को आसानी से resume नहीं किया जा सकता, इसलिए fallback को operation अंत तक संभालना पड़ता है
    • नतीजतन code duplication और complexity बढ़ती है
  • 2025-01-27 update में calling convention के जरिए इस समस्या को कम करने का तरीका जोड़ा गया
    • __attribute__((preserve_most)) fallback functions के लिए उपयोगी calling convention है, जो लगभग सभी register preservation की जिम्मेदारी callee पर डालकर spill cost को fallback side पर भेजता है
    • इस attribute से जुड़ा Clang crash bug 2023 में ठीक किया गया
    • __attribute__((preserve_none)) tail calling functions के लिए उपयोगी calling convention है, जो register preservation burden हटाता है और arguments के लिए अधिक registers देता है
    • दोनों में preserve_none को कम intrusive होने के कारण बेहतर विकल्प माना गया
  • एक और सीमा यह है कि musttail non-standard compiler extension है
    • उम्मीद है कि यह GCC, Visual C++ आदि तक फैले और standardize हो, लेकिन निकट भविष्य में ऐसा दिखता नहीं
    • musttail न होने पर conceptual loop की हर iteration पर कम-से-कम एक वास्तविक return चाहिए
    • upb में यह fallback अभी implement नहीं किया गया है, और अनुमान है कि musttail उपलब्ध होने पर dispatch को tail call करने, या अन्यथा सामान्य return करने वाले macro की जरूरत होगी

upb में वर्तमान स्थिति और विस्तार की संभावना

  • 2GB/s से अधिक वाला parser, C में लिखी गई छोटी protobuf library upb में submit किया गया
  • वह code पूरी तरह काम करता है और protobuf conformance test भी सभी pass करता है, लेकिन लिखे जाने के समय तक कहीं rollout नहीं हुआ था
  • C++ version protobuf में यह design implement नहीं किया गया था
  • बाद में upb को musttail उपयोग के लिए update किया गया, जिससे fast parser को production में लाने की एक बड़ी बाधा दूर हुई
  • यही technique C में लिखे प्रमुख language interpreters जैसे Python, Ruby, PHP, Lua आदि को भी महत्वपूर्ण performance लाभ दे सकती है

1 टिप्पणियां

 
GN⁺ 2024-08-20
Hacker News टिप्पणियाँ
  • C स्टैंडर्ड प्रस्ताव में tail call के लिए syntax है, और उसका रूप return goto (expression); है
    standard [[musttail]] की तुलना में इसमें मुझे जो बात पसंद है, वह यह है कि local object की lifetime खत्म होना guaranteed है। इसलिए इसे व्यापक escape analysis के बिना भी implement किया जा सकता है
    [0] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3266.htm#5...

    • जिज्ञासा है कि return goto को implement करना ज्यादा आसान क्यों है। [[musttail]] भी ऊपर से देखने पर local object की lifetime खत्म करता हुआ लगता है
      जल्दी से देखने पर लिखा है कि tail position में call किया गया function, callee के साथ same type का होना चाहिए। यह return value conversion की जरूरत खत्म करने और argument passing space तथा calling convention बने रहने की गारंटी के लिए शर्त है
      Clang में मैंने [[musttail]] implement किया था, और उसके बारे में जो शिकायत सबसे ज्यादा देखी, वह यह थी कि यह constraint बेवजह बहुत सख्त है। कुछ architectures type पूरी तरह match न होने पर भी tail call allow करते हैं: https://github.com/llvm/llvm-project/issues/54964
      “तो फिर code portable नहीं रहेगा” — यह बात सही है, लेकिन tail call optimization खुद ही मूल रूप से portable नहीं है। उदाहरण के लिए, WASM जैसे कुछ targets जिनमें tail call extension नहीं है, वे मूलतः tail call optimization support ही नहीं करते
    • C में नए features जोड़ने की कोशिश फिर से तेज होना उत्साहजनक भी है और काफी बेचैन करने वाला भी
      कुछ बदलाव, additions, और यहाँ तक कि clarify किए जाने वाले ideas ऐसे हैं जो सच में जरूरी लगते हैं, इसलिए उम्मीद भी है, लेकिन C++ का aggressive update cycle आखिरकार ऊपर से और परतें चढ़ती जाने जैसा बन गया लगता है
      खासकर तब समस्या होती है जब features आपस में उम्मीद से बहुत पहले बुरी तरह interact करने लगते हैं। उम्मीद है standardization process सिर्फ rationale documents पर निर्भर न रहे, बल्कि बड़े और विविध codebases में features को अच्छी तरह test करके बहुत conservative ढंग से चुने
  • अगर Rust में रुचि है, तो guaranteed tail call optimization देने के लिए become keyword जोड़ने का एक पुराना RFC है
    मूल रूप से इसे 2018 edition goals पर फोकस करने के लिए टाल दिया गया था, और वह फैसला सही था, लेकिन हाल में इस idea को फिर से review किया गया है। यह वापस आ सकता है
    [0]: https://github.com/rust-lang/rfcs/pull/1888
    [1]: https://github.com/rust-lang/rfcs/pull/3407

  • C++ में interpreter आम तौर पर इस तरह की speedup computed goto का इस्तेमाल करके पाते हैं। तब एक opcode से अगले opcode तक जाने वाले रास्ते में calling convention से जुड़ा शोर नहीं रहता
    computed goto तरीका हो या tail call तरीका, classical switch loop की तुलना में इनके तेज होने की मुख्य वजह branch predictor पर दबाव कम करना है। static रूप से opcode प्रति एक indirect branch बनती है, यह ऐसी संरचना नहीं रहती जिसमें static रूप से सिर्फ एक indirect branch हो

    • जैसा लेख में भी कहा गया है, computed goto इस्तेमाल करने पर भी function का control flow graph इतना जटिल हो सकता है कि अक्सर इस्तेमाल होने वाले variables की register allocation कमजोर पड़ जाती है
      अगर हर function छोटा हो और महत्वपूर्ण variables को arguments के रूप में ले, तो register allocation काफी कम नाजुक हो जाती है
    • “static रूप से opcode प्रति एक indirect branch” से आपका क्या मतलब है, यह जानने की उत्सुकता है। सिर्फ एक indirect branch होने की तुलना में इसका मतलब क्या है, और यह कैसे हासिल किया जाता है, थोड़ा और विस्तार से समझाना अच्छा होगा
    • सुना है कि यह तरीका कुछ समय से फैशन से बाहर हो गया था। कहा गया था कि branch predictor अब इतना अच्छा हो गया है कि इसकी जरूरत नहीं रही
      लेकिन interpreter का आकार बड़ा होने पर भी क्या यह बात सही रहती है, इसे लेकर जिज्ञासा है
  • context switching में tail call इस्तेमाल करने की बची हुई समस्या यह है कि इसमें calling convention का उपयोग करने वाले functions ही इस्तेमाल करने पड़ते हैं। दुर्भाग्य से function exit पर state restore करने के लिए registers बरबाद होते हैं
    विस्तार से analysis और एक intermediate compiler वाले alternative पर LuaJIT remake ब्लॉग में लिखा है: https://sillycross.github.io/2022/11/22/2022-11-22/

    • पिछले कुछ वर्षों में मैंने कुछ भाषाओं को JIT layer हटाते हुए और फिर वापस जोड़ते हुए देखा है। कुछ मामलों में यह programmer skill और सीखे गए lessons की वजह से था, लेकिन कुछ मामलों में CPU generation बदलना भी कारण था
      कंप्यूटर साइंस की बाकी लगभग हर चीज़ की तरह, जब अलग-अलग तरह के operations की cost balance बदलती है, तो सबसे अच्छा algorithm 15 या 20 साल पहले इस्तेमाल होने वाले तरीके पर वापस जा सकता है। इसी वजह से programming में trends जैसी चीज़ दिखाई देती है। किसी चीज़ को फिर से ज़िंदा करने का मतलब यह नहीं कि उसकी वजह नहीं है, लेकिन यह भूल जाना कि पिछली बार वह universal cure क्यों नहीं थी, फिर भी समस्या है
      अगर main JIT तेज या धीमा होता है, तो execution cost के मुकाबले उसका लाभ बदल जाता है, और उसे trigger करने वाली threshold भी adjust होती है। फिर दूसरे layer में execute होने वाले code की मात्रा बदलती है, और उस layer की amortized cost भी खराब हो सकती है। यह double pendulum का balance बनाने जैसा है
      अगर JIT layer को पर्याप्त तेज और rough बनाया जा सके, तो interpreter को पूरी तरह skip किया जा सकता है। बाहर से देखने पर interpreter और करीब दो JITs के बीच हिसाब मिलाने का cognitive load इतना बड़ा लगता है कि कुछ भाषाओं ने शायद interpreter को hold पर रखकर output speed की जगह compile time के लिए optimized JIT अपनाया
      याद नहीं कि कौन-सी भाषाएँ थीं, लेकिन कम-से-कम एक टीम ने इस balance problem की वजह से intermediate compiler भी आखिरकार हटा दिया था। तीन चीज़ों को संभालने की बजाय दो पर ध्यान देना बेहतर था
    • हाल में Clang में ऐसा नया calling convention आया है जो इस तरह के tail calls को काफी सस्ता बना देता है। यह caller को कुछ registers preserve करने की जरूरत से बचाता है
      नाम हर बार गड़बड़ा जाता है, शायद preserve_all या preserve_none है। समस्या यह है कि preservation किसके नज़रिए से कहा जा रहा है
  • मुझे लगता है musttail attribute GCC में जोड़ा जा रहा है। patch review में है, और semantics Clang के साथ compatible हैं

    • यह जानने की जिज्ञासा है कि preserve_most attribute का क्या होगा। क्या GCC में भी ऐसा कुछ आने की संभावना है? इसके बिना non-tail calls interpreter को बिगाड़ देते हैं
    • यह एक कठिन समस्या है। कई ABI बहुत बुनियादी मामलों में भी tail call नहीं कर पाते, जैसे argument और return type मेल खाने वाले external function call
      ऐसा लगता है कि Clang musttail calls के लिए calling sequence बदलने वाली heuristics रखता है। उदाहरण के लिए i686 पर यह noplt call में बदल देता है। Clang documentation में इसका ज़िक्र नहीं है: https://clang.llvm.org/docs/AttributeReference.html#musttail
      व्यावहारिक रूप से संभव बात शायद यही है कि जब compiler tail call generate नहीं कर सके, तब वह diagnostic message दे। बहुत से users के लिए शायद इतना ही काफी होगा। Scheme की तरह tail call की guarantee मिलना मुश्किल लगता है
    • GNUC में Scheme जैसी कई सुविधाएँ हैं, इसलिए इस feature में उसका पीछे रहना हैरानी की बात है
  • C++ support का भी उल्लेख है, लेकिन C++ में शायद tail calls बहुत कम होंगे
    उदाहरण के लिए foo() { auto a = SomeClassWithADestructor(); return bar(); } tail call नहीं है, क्योंकि bar() call के बाद a का destruction होता है

    • अगर compiler यह साबित कर सके कि उन पंक्तियों के बीच कोई दूरगामी side effect नहीं है, तो क्या वह bar चलाने से पहले destructor call नहीं कर सकता?
      यह जानने की उत्सुकता है कि क्या C++ standard सचमुच destructor को block के अंत में ही call करने को कहता है, या variable का उपयोग खत्म होते ही उसे call करना भी मान्य है
  • हो सकता है उदाहरण बहुत सरल हो, लेकिन अच्छे code generation के लिए __attribute__((musttail)) अनिवार्य नहीं लगता
    अगर error handling function rare path पर है, तो call speed भी शायद उतनी महत्वपूर्ण नहीं होगी
    if (unlikely(malformed)) return error(); switch (data_type) { case x: return handle_x(); case y: return handle_y(); } जैसी संरचना काफी स्थिर रूप से अच्छा jump table बनाती दिखती है

    • compiler लंबे समय से tail call elimination करता आया है, यह स्वाभाविक है, लेकिन इस technique में “काफी स्थिर रूप से बन जाता है” पर्याप्त नहीं है। यह या तो guaranteed होना चाहिए, या compilation fail होना चाहिए
      नहीं तो यह संरचना काम नहीं करेगी और stack तुरंत overflow हो जाएगा। [[musttail]] का पूरा मतलब यही है कि tail call elimination अनिवार्य है। compiler के पास कोई दूसरा विकल्प नहीं होना चाहिए
    • जैसा कि लेख का disassembly दिखाता है, fallback path की समस्या यह नहीं है कि call खुद कितना तेज़ है। सिर्फ उस call की मौजूदगी से भी compiler पूरे function के लिए stack frame बना सकता है और registers उसमें spill कर सकता है। असर fast path पर भी पड़ता है
      बेशक, “मजबूर करता है” कहना शायद पूरी तरह सटीक न हो। compiler के लिए यह तय नहीं है कि function के सभी execution paths में एक ही stack frame structure होना चाहिए, और न ही यह तय है कि जिन internal linkage functions या anonymous namespace functions का address नहीं लिया गया है, उनके लिए standard ABI ही इस्तेमाल करनी होगी। लेकिन मैंने जितने compilers देखे हैं, Clang समेत, वे व्यवहार में ऐसा ही करते हैं। इसलिए ऐसा कोई तरीका चाहिए जो यह बता सके कि ABI की चिंता छोड़ो और calls के बीच register preservation पर समय बर्बाद मत करो
      jump table तो निश्चित ही अच्छी तरह बन जाता है। लेकिन अगर उसके result को perf report जैसी किसी चीज़ से देखें, और test bytecode किसी छोटे loop का प्रतिनिधित्व न कर रहा हो, तो आम तौर पर दो में से एक चीज़ दिखेगी। या तो हर dispatch पर branch prediction miss होगा, या compiler यह समझकर कि “शायद interpreter लिखा जा रहा है”, indirect jump को हर case के अंत में खिसका देगा। Clang में मैंने ऐसा देखा है। दोनों ही स्थितियों में generated code का register allocation अक्सर बहुत खराब होने की संभावना है
  • यह जानने की जिज्ञासा है कि trampoline, यानी अगला function function pointer के रूप में return करना और उसे outer loop में call करना, कितना तेज़ होगा। इसका फायदा यह है कि यह portable C है

    • C का उपयोग अक्सर high-level language compilers की target language के रूप में होता है
      Scheme programming language यह मांग करती है कि सभी tail calls stack को न बढ़ाएँ। इसलिए implementers ने trampoline सहित कई techniques पर काम किया है
      मेरे पास उद्धृत करने के लिए सामग्री नहीं है, लेकिन Scheme को C में compile करने वाले papers में इसका उत्तर मिल सकता है। अगर target language tail call optimization की guarantee नहीं देती, तो generated program धीमा होगा
      अतिरिक्त रूप से, high-level language implementers के JavaScript spec से tail call optimization हटाए जाने पर नाराज़ होने का यही एक बड़ा कारण है। tail call optimization और stack inspection दोनों को बनाए रखने वाले समाधान भी मौजूद हैं
      https://github.com/schemedoc/bibliography/blob/master/page8....
    • tail call optimization तेज़ इसलिए है क्योंकि resulting loop predictable होता है, जिससे CPU instruction prefetch और memory prefetch अच्छी तरह काम करते हैं
      function pointer से jump करने पर शायद वह उतना predictable नहीं होगा, और वही लाभ मिलना कठिन होगा
      बेशक, इसे मापकर देखना होगा, और मैंने अभी तक ऐसा नहीं किया है
  • मैंने C में Protobuf decoder/encoder, IML parser, और Python bindings लिखकर देखे हैं, और parsing speed के measurement पर कुछ कहना है
    अगर यह library managed languages के लिए सिर्फ bindings के रूप में दी जाए, तो performance के मामले में एक अतिरिक्त variable आ जाता है जो बाकी सब पर भारी पड़ सकता है। Ruby या PHP के बारे में नहीं जानता, लेकिन Python में enumerator का उपयोग न करने पर मैंने नाटकीय speedup देखा। Protobuf enumerator को Python enumerator में बदलते ही C code से मिलने वाला कोई भी फायदा तरह-तरह के Python objects बनाने में लगने वाले समय के नीचे दब जाता है। अंतर कई orders of magnitude का है। इससे भी आगे बढ़कर, सभी auxiliary data structures को C में implement किया जा सकता है और Python को सिर्फ minimal interface expose किया जा सकता है। ऐसी तुलना Python built-in structures का उपयोग करने वाले code के मुकाबले कितनी fair है, इसका जवाब देना मुश्किल है
    Google का Python के लिए Protobuf parser अभी भी 2GB/s से “तेज़” हो सकता है। वजह यह है कि top-level message के अलावा वह वास्तव में कुछ भी parse नहीं करता। Message की internal structure जरूरत पड़ने पर parse होती है। अगर code parse की गई चीज़ों को तुरंत पूरा पढ़ ले, तो यह 2GB/s से धीमा होने की संभावना है, लेकिन सवाल यह है कि इन दोनों approaches की व्यावहारिक तुलना आखिर कैसे की जाए। Application के स्वभाव के अनुसार वास्तविक नतीजे बदलते हैं, इसलिए कोई साफ़ जवाब नहीं है
    सामान्य स्थिति में Protobuf parsing duplicate processing की वजह से streaming नहीं हो सकती। वास्तव में Protobuf content को parse करने वाला code I/O bottleneck से टकराता है। क्योंकि parsing शुरू करने से पहले message के अंत तक इंतज़ार करना पड़ता है। अलग बात यह है कि application के typical Protobuf messages के आधार पर parsing को parallelize करना संभव हो सकता है, और तब उसके single-threaded parser से आगे निकलने की संभावना काफ़ी होती है। लेकिन ऊपर के उदाहरणों की तरह, इसे भी आम तौर पर जीतने वाली strategy नहीं कहा जा सकता
    आम तौर पर parsing और domain object creation को साथ जोड़ना कहीं ज़्यादा efficient होता है। Application को लगभग हमेशा इस चरण से गुजरना ही पड़ता है। Parser इस functionality को कैसे approach कर सकता है, कई मामलों में यही तय करता है कि कौन-सा parser जीतेगा
    निष्कर्ष यह है कि Protobuf, और शायद parsers सामान्य रूप से भी, speed measurement और comparison target के रूप में अच्छे नहीं हैं। ये बहुत low-level हैं और इनका design भी इतना अच्छा नहीं कि इन्हें performance benchmark के मानक के रूप में लेना आसान हो

    • “सामान्य स्थिति में Protobuf parsing duplicate processing की वजह से streaming नहीं हो सकती” वाला हिस्सा समझ में नहीं आया
      अगर आप विस्तार से समझाएँ कि last field wins rule streaming parsing को कैसे रोकता है, तो अच्छा होगा
  • GCC और Clang में काफ़ी समय से -foptimize-sibling-calls option मौजूद है, इसलिए debug builds में भी tail calls मिल सकती थीं
    बेशक, इसका standardize होना, guarantee होना, और function level पर control किया जा सकना एक बड़ा सुधार है
    [1] https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
    [2] https://clang.llvm.org/docs/ClangCommandLineReference.html#t...