- अब तक Rust compiler की parallelization मुख्य रूप से Cargo और LLVM backend पर निर्भर थी, लेकिन अब frontend parallel execution भी जुड़ गई है, जिससे build के अंतिम हिस्से के bottleneck कम हो सकते हैं
- nightly में
-Z threads=8 से इस experimental feature को चालू किया जा सकता है, और default अब भी single-thread mode है, इसलिए explicit setting के बिना speed improvement नहीं मिलेगा
- नया frontend Rayon आधारित है और compile के सूक्ष्म कामों को बाँटता है; उदाहरण मापन में frontend time 10.2 सेकंड से घटकर 5.9 सेकंड हो गया
- वास्तविक code measurements में compile time अधिकतम 50% तक कम हुआ, लेकिन code की प्रकृति और build settings के अनुसार फर्क बड़ा हो सकता है, और memory usage अधिकतम 35% तक बढ़ सकता है
- यह feature अभी experimental stage में है, और
-Z threads को stable बनाना तथा stable में multi-thread default execution देना 2024 का लक्ष्य है
Rust compile parallelization का नया आयाम
- Rust compiler frontend अब parallel execution के जरिए compile time कम कर सकता है
- nightly compiler में
-Z threads=8 option देकर multi-thread frontend को आज़माया जा सकता है
- यह feature अभी experimental है, और stable compiler में शामिल करने का लक्ष्य 2024 है
मौजूदा optimization और बचे हुए bottleneck
- Compiler Performance Working Group कई वर्षों से Rust compiler की performance बेहतर कर रहा है
- 2023 के पहले 10 महीनों में performance measurement tools के आधार पर औसत compile time 13% घटा
- peak memory usage 15% कम हुई
- binary size 7% छोटी हुई
- compiler पहले से काफी optimized है, इसलिए बड़े बचे हुए improvements का मुख्य रास्ता parallelism बढ़ाना है
Cargo और backend parallelization की सीमाएँ
- Cargo, Rust program build करते समय कई
rustc processes चलाकर crate को parallel में compile करता है
-j1 flag से इस parallelization को बंद करने पर बड़े Rust programs का compile time काफी बढ़ जाता है
- Cargo का
--timings flag crate compile timeline chart बनाता है
- 28 virtual core वाली machine पर ripgrep build करने के उदाहरण में 60 process lines दिखाई देती हैं
- इनमें ज़्यादातर
rustc हैं और कुछ build scripts हैं
- शुरुआती 20 processes crate dependencies न होने के कारण एक साथ शुरू हो सकते हैं
- build के अंतिम हिस्से में crate dependencies बढ़ने से parallelism घटता जाता है
- pipelined compilation से dependent crate compilation कुछ हद तक overlap हो सकती है, लेकिन बड़े Rust programs में build के अंत की parallel execution फिर भी काफी कम हो जाती है
frontend और backend क्या करते हैं
- Rust compiler को मोटे तौर पर frontend और backend में बाँटा जाता है
- frontend parsing, type checking, borrow checking आदि करता है
- पुराना frontend parallel execution का उपयोग नहीं कर सकता था
- backend code generation संभालता है, और “codegen units” के आधार पर code बनाकर LLVM उसे parallel में process करता है
- release build में default codegen unit count 16 है, इसलिए उदाहरण profile में 16 LLVM threads दिखाई देते हैं
पुराने backend profile में दिखा bottleneck
- Samply से Cargo के अंतिम crate का release build मापने वाले उदाहरण में frontend को 10.2 सेकंड लगे
- backend को 6.2 सेकंड लगे, और LLVM threads उनमें से 5.9 सेकंड तक चले
- LLVM आधारित parallel code generation प्रभावी है, लेकिन 28-core machine पर भी 16 LLVM threads सभी एक साथ नहीं चले
- main thread, MIR को LLVM IR में बदलने का काम serial रूप में करता था, इसलिए codegen thread की शुरुआत में सीढ़ीनुमा pattern दिखा
- पूरी तरह serial चलने वाला frontend सबसे बड़ा improvement point बचा हुआ था
नए parallel frontend का implementation
- नया frontend Rayon का उपयोग करके बारीक स्तर के parallel compile tasks चलाता है
- कई data structures को mutex और read-write lock से synchronize किया गया है, और जहाँ ज़रूरत है वहाँ atomic types का उपयोग हुआ है
- frontend के कई tasks parallel किए गए हैं, लेकिन बदलाव अपेक्षाकृत कम महत्वपूर्ण बिंदुओं पर केंद्रित रहे
- frontend code का अधिकांश हिस्सा बदलने की ज़रूरत नहीं पड़ी
8 threads setting के measurement results
- parallel frontend को चालू करके 8 threads इस्तेमाल करने वाले उसी उदाहरण में frontend execution time 10.2 सेकंड से 5.9 सेकंड हो गया
- backend execution time 6.2 सेकंड से 5.3 सेकंड और LLVM thread execution time 5.9 सेकंड से 4.9 सेकंड तक घटा
- frontend में
rustc के रूप में दिखने वाले 7 अतिरिक्त threads काम करते हैं
- thread utilization समान नहीं है, और सभी 8 threads में idle sections हैं, इसलिए आगे improvement की गुंजाइश है
- 8 LLVM threads एक साथ शुरू होते हैं क्योंकि 8
rustc threads, 8 codegen units के LLVM IR को parallel में बनाते हैं
- frontend thread count को 16 करने पर सीढ़ीनुमा pattern पूरी तरह गायब हो जाता है, लेकिन उस case में अंतिम execution time लगभग नहीं बदलता
process-level और process के भीतर parallelization का संयोजन
- Rust compilation लंबे समय से Cargo की process-level parallelization और backend की process के भीतर parallelization से फायदा उठाती रही है
- अब frontend भी process के भीतर parallelization का लाभ उठा सकता है
- जब कई
rustc processes एक साथ चलें और हर process कई threads बनाए, तब jobserver protocol thread count को सीमित करता है
- process-level parallelism अधिक होने पर process के भीतर parallelism उसी हिसाब से घटता है, और कुल thread count core count से ऊपर नहीं जाता
उपयोग कैसे करें
- nightly compiler में parallel frontend शामिल है
- default अब भी single-thread mode है, इसलिए सामान्य रूप से compile time कम नहीं होगा
- multi-thread mode को
-Z threads option से explicit रूप से चालू करना होगा
RUSTFLAGS="-Z threads=8" cargo build --release
- एक या अधिक projects में
config.toml से setting करने के लिए यह जोड़ें
[build]
rustflags = ["-Z", "threads=8"]
- single-thread mode default होने का कारण सावधानीपूर्वक rollout है
- parallel frontend में काफी नया code है
- single-thread mode अधिकतर नया code चलाता है, लेकिन deadlock जैसे threading bugs की संभावना को अलग रखता है
- parallel program, Rust में भी, serial program की तुलना में सही लिखना अधिक कठिन होता है
- इसी कारण parallel frontend कुछ समय तक beta या stable release में शामिल नहीं होगा
performance और memory पर असर
- single-thread mode में parallel frontend आमतौर पर पुराने serial frontend से 0%~2% धीमा है
-Z threads=8 multi-thread mode में वास्तविक code measurements के अनुसार compile time अधिकतम 50% तक घट सकता है
- performance का असर code की प्रकृति और build settings के अनुसार बहुत बदलता है
- development build में release build की तुलना में अधिक बड़ा improvement दिख सकता है
- क्योंकि release build आमतौर पर backend optimization पर अधिक समय खर्च करती है
- कुछ छोटे programs जो पहले से तेज़ compile होते हैं, उनमें multi-thread mode single-thread mode से धीमा हो सकता है
- recommended value 8 threads है
- यह सबसे अधिक tested setting है और अच्छे नतीजे देने के लिए जानी जाती है
- 8 से कम value का लाभ कम है, लेकिन 8 से कम cores वाले hardware के लिए उपयुक्त हो सकती है
- 8 से अधिक value पर diminishing returns मिलते हैं और performance खराब भी हो सकती है
- 1 से 8 threads तक बढ़ाने पर भी improvement लगभग 50% रहने का कारण यह है कि frontend कुल compile time का केवल एक हिस्सा है और backend पहले से parallelized है
- multi-thread mode में memory usage काफ़ी बढ़ सकती है, और अधिकतम 35% बढ़ोतरी देखी गई है
correctness और feedback
- single-thread mode की reliability ऊँची होने की उम्मीद है
- multi-thread mode में deadlock सहित कुछ known bugs हैं
- अगर compilation रुक जाए, तो संभव है कि आप किसी known bug से टकराए हों
- किसी भी frontend का उपयोग हो, compiler द्वारा बनी binary एक जैसी होनी चाहिए; फर्क होने पर उसे bug माना जाएगा
- समस्या होने पर पहले
WG-compiler-parallel label issues देखें, और मेल खाता issue न मिले तो नया issue खोला जा सकता है
- सामान्य feedback wg-parallel-rustc Zulip channel में दिया जा सकता है, और वास्तविक code पर performance impact में विशेष रुचि है
2024 stable लक्ष्य
- parallel frontend की performance improvement पर काम जारी है
- profiles में दिखे अनुसार frontend thread utilization में अब भी improvement की गुंजाइश है
- multi-thread mode के बचे हुए bugs भी साफ किए जा रहे हैं
-Z threads option को stable बनाना और stable release में parallel frontend को multi-thread default के रूप में देना 2024 का लक्ष्य है
1 टिप्पणियां
Hacker News की राय
मुझे पता है कि यह अभी शुरुआती चरण में है, लेकिन Rust की कमी compile speed ही लगती है
Rust monorepo में काम करते समय मेरी सबसे बड़ी शिकायत compile speed थी, इसने CI/CD लागत बढ़ाई, और जब cache साफ़ करनी पड़ती थी तो development time भी काफ़ी धीमा हो जाता था
वजह Cargo नहीं बल्कि Docker bug था, लेकिन फिर भी इस तरह की प्रगति स्वागतयोग्य है
पहले ही बहुत optimization हो चुका है, और मौजूदा Rust compiler लगभग सभी mainstream compilers से ज़्यादा parallelism रखता है
Rust की language design खुद ही compile को Go जैसी उन भाषाओं की तुलना में कठिन बनाती है जिन्हें fast compile के लक्ष्य से बनाया गया था
यह काम वहाँ कितनी सीधे लागू होगी, पता नहीं, लेकिन वहाँ भी बड़ा सुधार हो तो अच्छा होगा
modern IDE support के लिए यह साफ़ तौर पर ज़रूरी हिस्सा है
मैं एक medium-scale open source Rust project [1] का maintainer हूँ, और local में Rust compile time मुझे हमेशा हैरान करने वाली हद तक तेज़ लगता है
MacBook Pro पर debug build कुछ सेकंड में हो जाता है, और release build व CI/CD धीमे हैं, लेकिन 2 साल पहले Rust शुरू करने के बाद से Rust compile मुझे काफ़ी तेज़ लगा है
संतुलन के लिए कहूँ तो, मेरा day job Java/Kotlin और Gradle है, और वहाँ तो compile time सचमुच हिमनद जैसा लगता है
अपने open source Rust project में मैं dependencies न्यूनतम रखता हूँ,
derive[Debug, Clone]जैसी चीज़ों के अलावा macros नहीं इस्तेमाल करता, और generics का उपयोग भी बहुत संयम से करता हूँअच्छा होगा अगर आप
cargo buildसे इस project को build करके compile time पर feedback दें[1]: https://github.com/Orange-OpenSource/hurl
और क्या project को सही बिंदुओं पर कई crates में बाँटा गया था
monorepo build में कितने मिनट लगते थे?
जितना ईमानदारी से बताना हो बताइए। मेरा मुख्य compiler GHC है, इसलिए आसानी से चौंकने वाला नहीं हूँ
यह शायद बेवकूफ़ी भरा सवाल हो, लेकिन क्या backend को frontend के borrow checking खत्म होने तक इंतज़ार करना पड़ता है? अगर हाँ, तो क्यों?
मेरा मतलब यह नहीं कि कुछ ग़लत है, बस यह जानना है कि क्या borrow checking साधारण correctness से आगे जाकर ऐसे invariants स्थापित करती है जिन पर backend निर्भर करता है
उदाहरण के लिए, अगर borrow check error आती है, तो क्या कोई वजह है कि speculative backend work नहीं किया जा सकता जिसे बाद में फेंक दिया जाए?
हालाँकि, संभव है कि वह सबसे optimized code न बनाए
जहाँ तक मुझे पता है, बदनाम
noaliasoptimization जैसी optimizations हैं जो borrow checking के दौरान स्थापित जानकारी का उपयोग करती हैं। इस optimization को enable करने के लिए भी कई कोशिशें करनी पड़ी थीं[1]NLL (non-lexical lifetimes) के साथ इसका रिश्ता भी पूरी तरह स्पष्ट नहीं है, लेकिन अगर backend के काम की जानकारी स्थापित करनी है तो शायद कम-से-कम कोई primitive borrow checker चाहिए होगा
लेकिन mrustc, NLL feature वाले Rust versions को भी borrow checker के बिना compile कर देता है, इसलिए यह अनिवार्य से ज़्यादा optimization से जुड़ी चीज़ लगती है
[0]: https://github.com/thepowersgang/mrustc
[1]: https://stackoverflow.com/a/57259339
अलग-अलग machines पर इस्तेमाल होने वाली config files में hardcoded values डालने के बजाय, क्या CPU core count इस्तेमाल करने का कोई तरीका है?
मेरा अनुमान है कि stable होने पर default core count ही होगा
यह काम अभी कहाँ तक पहुँचा है, पता नहीं, लेकिन एक समय योजना थी कि Cargo के rustc invocations के बीच jobserver से coordination हो, और तब default core count रखने वाली Cargo job count का इस्तेमाल होगा
Cargo negative values का भी समर्थन करता है, जिनसे core count में से घटाया जा सकता है
RUSTFLAGSenvironment variable इस्तेमाल किया जा सकता है, और लेख में भी यही है:बढ़िया! बहुत समय पहले जब मैंने Rust इस्तेमाल किया था, तब toy examples भी काफ़ी धीरे compile होते थे, लेकिन हाल में वापस आया तो Rust सचमुच बहुत बेहतर लगा, और अब जहाँ संभव हो वहाँ compile time की ज़्यादा परवाह किए बिना इसे इस्तेमाल कर रहा हूँ
हालाँकि, एक थोड़ा बड़ा project ऐसा है जहाँ साधारण बदलाव भी compile होने में 5 सेकंड से ज़्यादा लेने लगे, और पुरानी यादें लौट आईं
यहाँ तक सोचने लगा कि laptop के aircraft engine की तरह घूमने से पहले, दूसरे काम निपटाने तक save टाल दूँ ताकि analyzer चलना शुरू न करे
मेरे लिए यह सबसे बड़ा pain point है, इसलिए कोई भी प्रगति बहुत स्वागतयोग्य है
अच्छा है! library crate ecosystem के विपरीत, मेरे binary crates आम तौर पर बड़े और monolithic रहे हैं
अब मैं उन्हें कई library crates में बाँट रहा हूँ
इसका मतलब यह है कि compile के आख़िरी हिस्से में parallelization न होने के अलावा, सबसे बड़े crates sequentially process होते हैं, इसलिए यह बदलाव मुझे बहुत पसंद आया
कुछ साल Rust से आधा दूर रहकर Python या TypeScript जैसे environments में काम करने के बाद, हाल में एक project में इसे फिर इस्तेमाल किया, और compile speed लगभग तुरंत पूरी हो जाने जैसी थी
और बेहतर होना हमेशा अच्छा है, लेकिन यह पहले से ही काफ़ी शानदार स्थिति में है
आजकल ChatGPT जैसा cheat code भी है, इसलिए कुछ साल पहले जो मुश्किल Rust समस्याएँ मुझे रोक देतीं, उन्हें भी अब लगभग पार किया जा सकता है, और Rust का भविष्य काफ़ी अच्छा दिखता है
तब Docker image build में 60~90 मिनट लग जाते हैं, और तब एहसास होता है कि पूरे project की dependencies कितनी ज़्यादा हैं
क्या compiler को फिर से build किए बिना parallel compiler option बंद करने का कोई तरीका है?
मुझे इसकी ज़रूरत नहीं है, और मैंने वैसे भी codegen units को 1 पर सेट किया हुआ है, साथ ही यह शायद ऐसे ICE ट्रिगर कर रहा है जिन्हें मैं debug नहीं करना चाहता
मुझे पता है कि default 1 thread है, लेकिन मैं इसे पूरी तरह बंद करना चाहता हूँ
“multithreaded mode में deadlock समेत कुछ known bugs हैं. अगर compile अटक जाए, तो बहुत संभव है कि आपने उन्हीं में से किसी एक को hit किया हो.”
तो फिर
-Z threadsइस्तेमाल करने से पहले थोड़ा और इंतज़ार करूँगा ;)