Ruby की गति बढ़ाने के लिए C कोड को Ruby में फिर से लिखना
(jpcamara.com)- छोटे 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_yjithook के जरिए 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,Rangeclass औरeachmethod को C functionrange_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#timesC 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#times9 सेकंड तक घट गया, और Ruby 3.4 में 8 सेकंड का result भी आया
Integer#succ और VM-level optimization
Integer#timesimplementation increment operation के लिएi += 1की जगहi.succका उपयोग करता हैInteger#succinteger का अगला 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_tracePrimitive.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 सेकंड
whileexample उम्मीद से धीमा था, और यह implementation के तरीके की समस्या हो सकती हैfor inऔरarray#eachRuby VM bytecode level पर लगभग समान हैं, इसलिए performance भी मिलती-जुलती हैfor inआम तौर पर VM में#eachcall में बदल जाने वाली 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 सेकंड
- TruffleRuby 24.1
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 सेकंड
- C implementation
- यह implementation सभी
Rangecases को 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_yjitblock YJIT active होने पर ही संबंधित Ruby implementation लागू करता है- YJIT off हो तो C implementation चलती रहती है
- YJIT on हो तो YJIT-optimizable Ruby version का उपयोग होता है
- Ruby 3.3 से YJIT lazy initialization कर सकता है, और
with_yjitcode YJIT active होते ही सही method version लागू करता है with_yjitएक YJIT hook है, और call होने के बाद runtime मेंundef :with_yjitसे हटा दिया जाता है
YJIT द्वारा generate की गई machine code देखने का तरीका
- CRuby को
--enable-yjit=devoption के साथ build करने पर YJIT द्वारा generate की जाने वाली machine code disassembly देखी जा सकती है - build example इस प्रकार है
./configure --enable-yjit=devmake install
- run करते समय
--yjit-dump-disasmflag का उपयोग करें./ruby --yjit --yjit-dump-disasm test.rb 40
Integer#timesकाi.succVM 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 टिप्पणियां
Hacker News की राय
लूप का उदाहरण 1 अरब बार nested loop चलाने वाला एक अजीब benchmark लगता है, और अगर इसे हाथ से optimize किया जाए तो लगता है कि runtime का 99% से ज़्यादा हिस्सा शुरुआती भाग में सिमट जाएगा
अगर array के हर element के लिए liveness analysis किया जाए, तो लगता है कि पूरे outer loop को हटाया जा सकता है; सोच रहा हूँ कि क्या कोई compiler वास्तव में ऐसा analysis करता है
भले ही
ucompile time पर ज्ञात न हो, inner loop को भी शायद कुछ instructions में बदला जा सकता है, और यहclangजैसी तरफ़ जल्द आने वाला एक ज़्यादा standard optimization लगता हैtrack करने के लिए data बहुत ज़्यादा हो जाता है, और इसकी उपयोगिता शायद सिर्फ़ ऐसे गलत code जैसे मामलों में ही होती है
पहले जब AI compiler बनाया था, तब tensor के individual elements का liveness analysis वास्तव में उपयोगी हो सकता था, फिर भी compile time और memory requirements इतनी बेहिसाब थीं कि यह नहीं किया गया
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...
याद पड़ता है कि performance improvements point releases में भी आ सकती थीं
Integer#succको मैं performance के लिए ही नहीं बल्कि readability के लिए भी अक्सर इस्तेमाल करता हूँउदाहरण के लिए, UUID library के
#bytesmethod में 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 के क़रीब स्तर का है, तो यह काफ़ी चौंकाने वाला है
विवरण के लिए https://github.com/oracle/truffleruby देखें
Ruby सच में काफ़ी तेज़ हो गया है, और खासकर TruffleRuby तो और भी प्रभावशाली है
अफ़सोस की बात है, क्योंकि Ruby performance पर सबसे बड़ा असर डालने वाला संयोजन शायद यही होता
मुझे पता नहीं था कि YJIT Rust में लिखा गया है, यह सच में दिलचस्प है
कमी यह है कि अगर सही toolchain या platform न हो तो build के समय यह enable न हो, लेकिन फिर भी यह एक ठीक-ठाक tradeoff लगता है
एक और language-comparison repository भी है जो ज़्यादा समय से चल रही है और ज़्यादा भाषाओं को कवर करती है: https://github.com/niklas-heer/speed-comparison
chart axis labels और bar labels एक-दूसरे पर चढ़ रहे हैं, और vertical grid lines भी नहीं हैं
काश यह एक simple HTML table होती
यह दिलचस्प है कि benchmark में Python सबसे धीमी भाषा थी, फिर भी अक्टूबर 2024 तक GitHub पर सबसे ज़्यादा इस्तेमाल होने वाली भाषा वही है
ऐसा लगता है जैसे धीमी भाषाएँ ज़्यादा लोकप्रिय होती हैं
मैं Rust को काफ़ी गहराई से पसंद करता हूँ, लेकिन हर भाषा, runtime, interpreter और compiler एक tool है
समस्या और समाधान के तरीके के अनुसार अच्छे tools का सेट चाहिए, और अगर कोई program कुछ ही बार चलने वाला है, तो धीमी execution speed उतनी मायने नहीं रखती
Python और R जैसे programs में ऐसे मामले काफ़ी होते हैं
कुल मिलाकर Ruby धीमी है, लेकिन उसमें coding करना इतना अच्छा लगता है कि यह नए लोगों के लिए ज़्यादा आकर्षक बनती है
मेरे Advent of Code solutions हैरान करने वाली हद तक ऐसे ही दिखते हैं, इसलिए यह खेल बदल देने वाला बदलाव लगता है