2 पॉइंट द्वारा GN⁺ 2024-12-06 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • छोटे language benchmark में CRuby नीचे से तीसरे स्थान पर था, लेकिन इससे यह दिखा कि bottleneck पूरी भाषा में नहीं, बल्कि ऐसे iteration implementation में है जिसे YJIT अंदर से नहीं देख सकता
  • Ruby 3.3.6 ने M3 MacBook Pro पर Fibonacci में 12.17 सेकंड और Loops में 28.80 सेकंड लिए, जबकि node.js दोनों में 1 सेकंड के दायरे में था; M2 MacBook Air पर Ruby क्रमशः 16.33 सेकंड और 33.43 सेकंड के साथ और धीमा था
  • केवल ruby --yjit चालू करने पर Fibonacci 16.88 सेकंड से घटकर 2.06 सेकंड हो जाता है, लेकिन Loops 33.43 सेकंड से सिर्फ 25.57 सेकंड तक घटता है, इसलिए Range#each bottleneck बना रहता है
  • Ruby 3.3 के Integer#times और Ruby 3.4 के Array#each जैसे जिन methods की C implementation को Ruby की ओर ले जाया गया, वे YJIT optimization के target बन गए और iteration benchmark 13~14 सेकंड के स्तर तक सुधर गया
  • CRuby with_yjit hook के जरिए YJIT active होने पर Ruby implementation इस्तेमाल कर सकता है और inactive होने पर C implementation बनाए रख सकता है, जिससे core library को YJIT-friendly Ruby code में बदलने का चलन बन रहा है

Benchmark में दिखी Ruby की स्थिति

  • हाल ही में साझा किया गया language comparison repo कई भाषाओं के छोटे benchmarks को सहयोग से बनाने वाला repository है
  • इस repository के CRuby नतीजों में Ruby से धीमे केवल R और Python थे, यानी Ruby नीचे से तीसरे स्थान पर थी
  • Benchmark दो पहलुओं को देखता है
    • Loops: loops, conditionals और basic math operations की performance पर जोर देता है
    • Fibonacci: function call overhead और recursion cost को उजागर करता है
  • Loops example nested loops के जरिए कुल 10,000 × 100,000 बार, यानी 1 अरब iterations करता है
  • Fibonacci example optimized implementation नहीं, बल्कि जानबूझकर सरल naive Fibonacci का उपयोग करता है

शुरुआती माप और YJIT लगाने का असर

  • M3 MacBook Pro पर Ruby 3.3.6 का स्तर यह था
    • Fibonacci: 12.17 सेकंड
    • Loops: 28.80 सेकंड
    • node.js: दोनों examples में 1 सेकंड से थोड़ा ज्यादा
  • M2 MacBook Air पर वही benchmark और धीमा निकला
    • Ruby Fibonacci: 16.33 सेकंड
    • Ruby Loops: 33.43 सेकंड
    • node.js Fibonacci: 1.36 सेकंड
    • node.js Loops: 2.07 सेकंड
  • मूल repository का run command YJIT के बिना ruby ./code.rb 40 के रूप में था
  • ruby --yjit ./code.rb 40 से चलाने पर नतीजे बदल जाते हैं
    • Fibonacci: 2.06 सेकंड
    • Loops: 25.57 सेकंड
  • YJIT ने Fibonacci में बड़ा असर दिखाया, लेकिन Loops में सुधार सीमित रहा

Range#each YJIT के लिए प्रतिकूल क्यों है

  • Loops code का मुख्य iteration (0...10_000).each और (0...100_000).each जैसे Range#each रूपों में है
  • Ruby 3.4 के हिसाब से Range#each अभी भी C में लिखा गया है
  • CRuby का range.c, Range class और each method को C function range_each से जोड़ता है
  • range_each कई range forms को संभालने के लिए अलग-अलग paths में branch करता है
    • (0...).each
    • (0...100).each
    • ("a"..."z").each
  • C function खुद तेज हो सकता है, लेकिन YJIT उसके अंदर नहीं देख सकता
    • optimization function call point पर रुक जाता है और C function के return होने के बाद फिर जारी होता है
    • YJIT hot execution paths के लिए खास optimization बना सकता है, लेकिन C implementation इस लाभ को सीमित कर देती है

Integer#times में बदलने पर बदलाव

  • Ruby 3.3 में Integer#times C function से Ruby method में बदला गया
  • इसका core structure while i < self, yield i, i = i.succ से बना एक सरल Ruby loop है
  • Ruby code में लिखे होने की वजह से YJIT अंदरूनी हिस्से को analyze और optimize कर सकता है
  • Range iteration को 10_000.times और 100_000.times में बदलने पर Loops time काफी घट जाता है
    • Range#each: 25.57 सेकंड
    • Integer#times: 13.66 सेकंड
  • दूसरी measurement environment में Integer#times 9 सेकंड तक घट गया, और Ruby 3.4 में 8 सेकंड का result भी आया

Integer#succ और VM-level optimization

  • Integer#times implementation increment operation के लिए i += 1 की जगह i.succ का उपयोग करता है
  • Integer#succ integer का अगला value लौटाने वाला method है
  • Ruby VM bytecode में i.succ को opt_succ के एक step के रूप में represent किया जाता है
  • इसके उलट i += 1 दो steps में बंटता है
    • putobject_INT2FIX_1_: integer 1 को VM stack पर रखता है
    • opt_plus: + operation करता है
  • सामान्य Ruby programs में इस पर ध्यान देने की जरूरत शायद ही पड़े, लेकिन JIT और VM level पर जब यह लाखों से अरबों बार repeat हो, तो एक step का फर्क भी performance को प्रभावित करता है

Array#each भी Ruby 3.4 में Ruby implementation में गया

  • Ruby 3.4 में Array#each भी C से Ruby-side implementation में बदला गया
  • पहला प्रयास सरल Ruby code था, लेकिन CRuby internals से जुड़ी एक race condition थी
  • final implementation Ruby code के अंदर Primitive का उपयोग करता है
    • Primitive.attr! :inline_block, :c_trace
    • Primitive.cexpr!
    • ary_fetch_next
  • यह पूरी तरह pure Ruby से अधिक C code evaluation और Ruby structure का मिला-जुला रूप है, लेकिन YJIT फिर भी काफी optimization कर सकता है
  • Array को पहले से बनाकर Array#each से repeat करने पर Integer#times जैसी performance मिलती है
    • Range#each: 25.57 सेकंड
    • Integer#times: 13.66 सेकंड
    • Array#each: 13.96 सेकंड

Ruby Microbench measurement

  • अलग repository ruby_microbench मूल example और कई Ruby iteration forms की तुलना करता है
  • Ruby 3.4 में YJIT on करने पर नतीजे ये हैं
    • Fibonacci: 2.19 सेकंड
    • array#each: 14.02 सेकंड
    • range#each: 26.61 सेकंड
    • times: 13.12 सेकंड
    • for: 14.91 सेकंड
    • while: 37.10 सेकंड
    • loop do: 13.95 सेकंड
  • Ruby 3.4 में YJIT off करने पर ज्यादातर चीजें धीमी हो जाती हैं
    • Fibonacci: 16.49 सेकंड
    • array#each: 34.29 सेकंड
    • range#each: 33.88 सेकंड
    • times: 33.18 सेकंड
    • for: 36.32 सेकंड
    • while: 37.14 सेकंड
    • loop do: 50.65 सेकंड
  • while example उम्मीद से धीमा था, और यह implementation के तरीके की समस्या हो सकती है
  • for in और array#each Ruby VM bytecode level पर लगभग समान हैं, इसलिए performance भी मिलती-जुलती है
    • for in आम तौर पर VM में #each call में बदल जाने वाली syntax sugar के करीब है

अन्य Ruby implementations से तुलना

  • वही benchmark कई Ruby implementations पर भी चलाया गया
  • कुछ नतीजे इस प्रकार हैं
    • TruffleRuby 24.1
      • Fibonacci: 0.92 सेकंड
      • array#each: 0.97 सेकंड
      • range#each: 0.92 सेकंड
      • times: 2.39 सेकंड
      • for: 2.06 सेकंड
      • while: 3.90 सेकंड
      • loop do: 0.77 सेकंड
    • MRuby 3.3
      • Fibonacci: 28.83 सेकंड
      • array#each: 144.65 सेकंड
      • range#each: 126.40 सेकंड
      • times: 128.22 सेकंड
    • Artichoke
      • Fibonacci: 19.71 सेकंड
      • array#each: 236.10 सेकंड
      • range#each: 214.55 सेकंड
      • times: 214.51 सेकंड

Range#each को Ruby से monkey patch करने का प्रयोग

  • Range#each को सरल Ruby implementation से monkey patch करने पर performance काफी सुधरती है
  • implementation begin, end, loop, yield, i.succ का उपयोग करने वाला सरल रूप है
  • measurements इस प्रकार हैं
    • C implementation Range#each: 25.57 सेकंड
    • Ruby implementation Range#each: 16.64 सेकंड
  • यह implementation सभी Range cases को handle नहीं करने वाला बहुत ज्यादा simplified version है
  • फिर भी C से Ruby में ले जाने पर YJIT optimize कर सकता है, और सामान्य C code से दोहराना मुश्किल या असंभव performance improvement संभव हो जाता है

YJIT standard library और with_yjit

  • Aaron Patterson का Ruby Outperforms C GraphQL parsing के लिए C extension को Ruby में फिर से लिखने का case है, जहां YJIT optimization की वजह से Ruby code C से तेज था
  • CRuby की core YJIT team कुछ core functions में C code हटाने या YJIT on होने पर ही Ruby implementation इस्तेमाल करने का तरीका अपना रही है
  • with_yjit block YJIT active होने पर ही संबंधित Ruby implementation लागू करता है
    • YJIT off हो तो C implementation चलती रहती है
    • YJIT on हो तो YJIT-optimizable Ruby version का उपयोग होता है
  • Ruby 3.3 से YJIT lazy initialization कर सकता है, और with_yjit code YJIT active होते ही सही method version लागू करता है
  • with_yjit एक YJIT hook है, और call होने के बाद runtime में undef :with_yjit से हटा दिया जाता है

YJIT द्वारा generate की गई machine code देखने का तरीका

  • CRuby को --enable-yjit=dev option के साथ build करने पर YJIT द्वारा generate की जाने वाली machine code disassembly देखी जा सकती है
  • build example इस प्रकार है
    • ./configure --enable-yjit=dev
    • make install
  • run करते समय --yjit-dump-disasm flag का उपयोग करें
    • ./ruby --yjit --yjit-dump-disasm test.rb 40
  • Integer#times का i.succ VM bytecode में opt_succ के रूप में दिखता है
  • YJIT की Rust implementation opt_succ के लिए यह करती है
    • receiver Fixnum है या नहीं, इसका guard करती है
    • Fixnum नहीं होने पर दूसरे execution path में निकल जाती है
    • Fixnum होने पर internal tag representation की वजह से 1 जोड़ने के लिए असल में 2 जोड़ती है
    • overflow होने पर दूसरे path में निकल जाती है
  • यह example दिखाता है कि JIT optimization Ruby code, C code, VM bytecode, Rust implementation और machine code जैसी कई layers के बीच आ-जा कर काम करता है

CRuby optimization की दिशा

  • Ruby implementation का काम Ruby से lower-level languages में काफी होता है, और CRuby में मुख्य रूप से C और कुछ Rust का उपयोग होता है
  • YJIT जैसी layer अधिक language features को सामान्य Ruby code में ले जाने की संभावना बनाती है
  • core functions अधिक Ruby में लिखे जाने पर Ruby developers के लिए CRuby में contribute करना आसान हो सकता है
  • Java की तरह छोटे low-level core के ऊपर language का ज्यादातर हिस्सा खुद उसी भाषा में लिखा हो—ऐसा structure CRuby के संभावित भविष्य में से एक माना जाता है
  • मौजूदा प्रवाह में C implementation को जस का तस बनाए रखने के बजाय, YJIT द्वारा optimize की जा सकने वाली Ruby implementation को core paths में लाना लगातार महत्वपूर्ण है

1 टिप्पणियां

 
GN⁺ 2024-12-06
Hacker News की राय
  • लूप का उदाहरण 1 अरब बार nested loop चलाने वाला एक अजीब benchmark लगता है, और अगर इसे हाथ से optimize किया जाए तो लगता है कि runtime का 99% से ज़्यादा हिस्सा शुरुआती भाग में सिमट जाएगा
    अगर array के हर element के लिए liveness analysis किया जाए, तो लगता है कि पूरे outer loop को हटाया जा सकता है; सोच रहा हूँ कि क्या कोई compiler वास्तव में ऐसा analysis करता है
    भले ही u compile time पर ज्ञात न हो, inner loop को भी शायद कुछ instructions में बदला जा सकता है, और यह clang जैसी तरफ़ जल्द आने वाला एक ज़्यादा standard optimization लगता है

    • compiler आम तौर पर array के individual elements के लिए liveness analysis नहीं करते
      track करने के लिए data बहुत ज़्यादा हो जाता है, और इसकी उपयोगिता शायद सिर्फ़ ऐसे गलत code जैसे मामलों में ही होती है
      पहले जब AI compiler बनाया था, तब tensor के individual elements का liveness analysis वास्तव में उपयोगी हो सकता था, फिर भी compile time और memory requirements इतनी बेहिसाब थीं कि यह नहीं किया गया
    • ज़्यादातर मामलों में इसे closed form में निकाला जा सकता है:
      result = ((u * (u - 1)) / 2 * (100000/u)) + (100000%u * (100000%u - 1) / 2) + r)
  • लेख आने वाले Ruby versions की बात कर रहा है, और जिज्ञासा में देखा तो ruby 3.4.0 इस क्रिसमस पर और ruby 3.5.0 अगले क्रिसमस पर आने की उम्मीद लगती है
    यह भी दिलचस्प होगा कि ऐसे loops में Python का minimal JIT क्या असर डालता है
    Python 3.13 में JIT enable करके build करना पड़ता है, इसलिए अगर कोई built environment में benchmark चलाए तो देखना रोचक होगा: https://drew.silcock.dev/blog/everything-you-need-to-know-ab...

    • Ruby हमेशा क्रिसमस पर release होता है, इसलिए यह एक predictible और प्यारा schedule है
      याद पड़ता है कि performance improvements point releases में भी आ सकती थीं
  • Integer#succ को मैं performance के लिए ही नहीं बल्कि readability के लिए भी अक्सर इस्तेमाल करता हूँ
    उदाहरण के लिए, UUID library के #bytes method में code पढ़ते समय दिमाग़ को “bit slicing mode” में बनाए रखने के लिए मैंने इसे दो बार इस्तेमाल किया
    16-iteration loop को 0xF.succ और loop के अंदर 256 से भाग देने वाले हिस्से को 0xFF.succ के रूप में लिखा: https://github.com/okeeblow/DistorteD/blob/ba48d10/Globe%20G...

    • जानना चाहता हूँ कि इस मामले में 0xF.succ आपको 0x10 से बेहतर क्यों लगता है
  • मैं https://github.com/bddicken/languages में योगदान दे रहा हूँ, और Lua approach आज़माने के बाद कहीं उल्लेखित TruffleRuby भी याद आया, लेकिन main.rb चलाने पर TruffleRuby और सामान्य Ruby के बीच व्यावहारिक रूप से बड़ा अंतर नहीं दिखा
    कभी-कभी सामान्य Ruby, TruffleRuby से तेज़ भी थी
    मैं देखना चाहूँगा कि लेख में दिया गया TruffleRuby speed benchmark किस बदलाव के बाद का है, और अगर इसे verify कर पाया तो main repository में commit के रूप में जोड़ना चाहूँगा
    अगर TruffleRuby implementation सचमुच Node.js से तेज़ और Bun या Go के क़रीब स्तर का है, तो यह काफ़ी चौंकाने वाला है

    • TruffleRuby में startup time और peak performance तक पहुँचने के समय को ध्यान में रखना पड़ता है, और यह native/JVM runtime settings के हिसाब से बदलता है
      विवरण के लिए https://github.com/oracle/truffleruby देखें
  • Ruby सच में काफ़ी तेज़ हो गया है, और खासकर TruffleRuby तो और भी प्रभावशाली है

    • वह Oracle का है: https://github.com/oracle/truffleruby
    • Rails, Truffle पर नहीं चलता, और मेरी समझ के अनुसार कुछ समय तक यह मुश्किल ही रहेगा
      अफ़सोस की बात है, क्योंकि Ruby performance पर सबसे बड़ा असर डालने वाला संयोजन शायद यही होता
  • मुझे पता नहीं था कि YJIT Rust में लिखा गया है, यह सच में दिलचस्प है

    • शुरुआत में यह C में लिखा गया था, फिर Rust में port किया गया, और यह अच्छा फ़ैसला लगता है
      कमी यह है कि अगर सही toolchain या platform न हो तो build के समय यह enable न हो, लेकिन फिर भी यह एक ठीक-ठाक tradeoff लगता है
  • एक और language-comparison repository भी है जो ज़्यादा समय से चल रही है और ज़्यादा भाषाओं को कवर करती है: https://github.com/niklas-heer/speed-comparison

    • यह भी एक language-comparison repository है, लेकिन इसकी presentation पढ़ने में मुश्किल है
      chart axis labels और bar labels एक-दूसरे पर चढ़ रहे हैं, और vertical grid lines भी नहीं हैं
      काश यह एक simple HTML table होती
  • यह दिलचस्प है कि benchmark में Python सबसे धीमी भाषा थी, फिर भी अक्टूबर 2024 तक GitHub पर सबसे ज़्यादा इस्तेमाल होने वाली भाषा वही है
    ऐसा लगता है जैसे धीमी भाषाएँ ज़्यादा लोकप्रिय होती हैं

    • अगर दोबारा तुलना करनी है, तो compile time भी शामिल करना चाहिए, और उसे उस build के अपेक्षित run count पर amortize करना चाहिए
      मैं Rust को काफ़ी गहराई से पसंद करता हूँ, लेकिन हर भाषा, runtime, interpreter और compiler एक tool है
      समस्या और समाधान के तरीके के अनुसार अच्छे tools का सेट चाहिए, और अगर कोई program कुछ ही बार चलने वाला है, तो धीमी execution speed उतनी मायने नहीं रखती
      Python और R जैसे programs में ऐसे मामले काफ़ी होते हैं
    • यह खाने जैसा है, ऊपर चीनी डाल दो तो लोगों को बहुत ज़्यादा पसंद आता है
      कुल मिलाकर Ruby धीमी है, लेकिन उसमें coding करना इतना अच्छा लगता है कि यह नए लोगों के लिए ज़्यादा आकर्षक बनती है
    • धीमी भाषाएँ ज़्यादा high-level होती हैं, इसलिए इस्तेमाल में आसान होती हैं
    • सोच रहा हूँ कि क्या top 20 लोकप्रिय भाषाओं को देखने पर भी यह संबंध बना रहता है
  • मेरे Advent of Code solutions हैरान करने वाली हद तक ऐसे ही दिखते हैं, इसलिए यह खेल बदल देने वाला बदलाव लगता है