1 पॉइंट द्वारा GN⁺ 2025-06-28 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Rust से बनी वेबसाइट को Docker में बार-बार build करते समय build time की समस्या सामने आई
  • डिफ़ॉल्ट Docker सेटअप में हर बार पूरी dependency दोबारा rebuild होती है, जिससे 4 मिनट से ज़्यादा लगते हैं
  • cargo-chef और caching tools इस्तेमाल करने पर भी अंतिम binary build में अब भी काफी समय लगता है
  • profiling के नतीजे में पता चला कि ज़्यादातर समय LTO(link-time optimization) और LLVM module optimization में खर्च होता है
  • optimization options, debug information, और LTO settings को adjust करके कुछ सुधार संभव है, लेकिन अंतिम binary compile होने में कम-से-कम 50 सेकंड तो लगते ही हैं

समस्या और पृष्ठभूमि

  • Rust में बनी निजी वेबसाइट को हर बार बदलने पर statically linked binary build करके server पर कॉपी करना और फिर restart करना एक बार-बार होने वाला झंझट था
  • Docker या Kubernetes जैसी container-based deployment में जाना चाहा, लेकिन Rust की Docker build speed एक बड़ी समस्या बनकर सामने आई
  • Docker के अंदर छोटे code change पर भी सब कुछ शुरू से rebuild करना पड़ता था, जिससे काफ़ी inefficiency होती थी

Docker में Rust build – बुनियादी तरीका

  • आम Dockerfile तरीका यह है कि सभी dependencies और source code को copy करने के बाद cargo build चलाया जाए
  • इस स्थिति में caching का फ़ायदा नहीं मिलता और पूरा rebuild बार-बार होता है
  • लेखक की वेबसाइट के हिसाब से full build में लगभग 4 मिनट लगते थे—dependency download में अलग से समय लगता था

Docker build caching में सुधार – cargo-chef

  • cargo-chef tool का इस्तेमाल करने पर केवल dependencies को अलग layer में पहले से cache किया जा सकता है
  • इससे code change होने पर dependency build दोबारा इस्तेमाल हो सकती है और build speed बेहतर होने की उम्मीद रहती है
  • वास्तविक उपयोग में, कुल समय का सिर्फ 25% dependency build पर गया, और अंतिम web service binary build में अब भी काफ़ी समय लगा (2 मिनट 50 सेकंड~3 मिनट)
  • प्रमुख dependencies (axum, reqwest, tokio-postgres आदि) और लगभग 7,000 lines के अपने code के बावजूद, rustc की एक single run में 3 मिनट लग रहे थे

rustc build time analysis: cargo --timings

  • cargo --timings का उपयोग करके हर crate (compilation unit) का build time देखा जा सकता है
  • नतीजे में साफ़ हुआ कि अंतिम binary build ही कुल समय का बड़ा हिस्सा ले रहा था
  • यह अधिक बारीक वजहें खोजने में मदद करता है, लेकिन compiler के अंदर क्या हो रहा है, यह पूरी तरह नहीं बताता

rustc self-profiling (-Zself-profile) का उपयोग

  • rustc की self-profiling सुविधा को -Zself-profile flag से चालू कर के अंदरूनी चरणों का समय मापा गया
  • इसके लिए environment variable के ज़रिए profiling enable की गई
  • summarize tool से analysis करने पर पता चला कि LLVM LTO(link-time optimization) और LLVM module code generation कुल समय का 60% से ज़्यादा ले रहे थे
  • flamegraph visualization में भी codegen_module_perform_lto चरण में कुल समय का 80% खर्च होता दिखा

LTO(link-time optimization) और build optimization options

  • Rust build आम तौर पर पहले अलग-अलग codegen unit में बाँटी जाती है, फिर LTO द्वारा बाद के चरण में global optimization लागू होती है
  • LTO में off, thin, fat जैसे कई options होते हैं, जिनका performance और final output पर असर पड़ता है
  • लेखक के project में Cargo.toml में LTO thin पर और debug symbols full पर सेट थे
  • अलग-अलग LTO/debug symbol combinations को test करने पर:
    • full debug symbols से build time बढ़ता है, और fat LTO में build लगभग 4 गुना धीमा हो जाता है
    • LTO और debug symbols हटाने पर भी कम-से-कम 50 सेकंड का build time बना रहता है

अतिरिक्त optimization और विचार

  • लगभग 50 सेकंड लेखक की कम-traffic वाली साइट के लिए कोई बड़ी समस्या नहीं थी, लेकिन तकनीकी जिज्ञासा के कारण आगे analysis किया गया
  • अगर incremental compilation को Docker के साथ अच्छी तरह इस्तेमाल किया जाए, तो build और तेज़ हो सकती है, लेकिन इसके लिए clean build environment और Docker cache का सही मेल ज़रूरी है

LLVM चरण की विस्तृत profiling

  • LTO और debug symbols हटाने के बाद भी LLVM_module_optimize चरण में लगभग 70% समय जा रहा था
  • release profile में opt-level का डिफ़ॉल्ट मान (3) optimization cost बढ़ा रहा था; इसलिए केवल binary के लिए opt-level कम करने का तरीका test किया गया
  • अलग-अलग optimization combinations के प्रयोग में, optimization बंद (opt-level=0) होने पर build time लगभग 15 सेकंड, जबकि optimization (1~3) पर लगभग 50 सेकंड रहा

LLVM trace events का गहरा analysis

  • rustc के अतिरिक्त flags (-Z time-llvm-passes, -Z llvm-time-trace) से LLVM के हर चरण का विस्तृत timing trace किया जा सकता है
  • -Z time-llvm-passes का output बहुत बड़ा होता है, इसलिए कई बार Docker की log limit पार हो जाती है और log settings बदलनी पड़ती हैं
  • logs को file में save करके analysis करने पर हर LLVM optimization pass का execution time अलग-अलग देखा जा सकता है
  • -Z llvm-time-trace option chrome tracing format में बहुत बड़ा JSON output बनाता है, जिसे सामान्य text editor या analysis tools में संभालना मुश्किल होता है
  • इसे newline-आधारित processing (jsonl) में बाँटकर CLI/script environment में analyze किया जा सकता है

मुख्य insight और निष्कर्ष

  • Docker में जटिल Rust project build करते समय build speed का bottleneck मुख्यतः अंतिम binary build और उससे जुड़े LLVM optimization चरणों में होता है
  • LTO, debug symbols, और opt-level को adjust करते समय build time और binary size के बीच स्पष्ट trade-off दिखाई देता है
  • optimization options को आक्रामक रूप से adjust करने पर build time काफ़ी कम हो सकता है, लेकिन optimization बंद करने पर performance गिरने की संभावना रहती है
  • अगर बड़े crate dependencies और production environment में build efficiency महत्वपूर्ण है, तो profiling का सक्रिय उपयोग करके विस्तृत bottleneck को ठोस रूप से समझना बेहतर रणनीति है
  • Rust build pipeline design करते समय LTO, opt-level, debug symbols, और cache strategy के बारीक संयोजन की योजना बनाना ज़रूरी है

1 टिप्पणियां

 
GN⁺ 2025-06-28
Hacker News की राय
  • Rust प्रोजेक्ट्स अक्सर ऊपर से छोटे दिखते हैं, इसलिए यह दिलचस्प लगता है। पहला, dependency हमेशा codebase के वास्तविक आकार से जुड़ी नहीं होती। C++ में बड़े प्रोजेक्ट की dependencies को अक्सर vendoring कर लिया जाता है या फिर बिल्कुल इस्तेमाल नहीं किया जाता, इसलिए अगर 4 लाख lines of code में बहुत-सा हिस्सा धीमा हो तो कोई सोच सकता है, "कोड बहुत है, इसलिए धीमा होना स्वाभाविक है।" दूसरा, इससे भी बड़ी समस्या macro हैं। 10 या 100 लाइनों को बार-बार expand करने वाले macro, 10 हज़ार लाइनों के प्रोजेक्ट को बहुत जल्दी 10 लाख लाइनों जैसा बना सकते हैं। तीसरा generic हैं। हर generic instantiation CPU resources खाती है। फिर भी थोड़ा बचाव करूँ तो, इन्हीं फीचर्स की वजह से C में 1 लाख लाइन, C++ में 25 हज़ार लाइन वाला काम Rust में कुछ हज़ार लाइनों में सिमट सकता है। लेकिन यह भी सच है कि इन फीचर्स के अत्यधिक इस्तेमाल से ecosystem धीमा दिखता है। उदाहरण के लिए, हमारी कंपनी async-graphql इस्तेमाल करती है; library खुद शानदार है, लेकिन procedural macro पर इसकी निर्भरता बहुत ज़्यादा है। performance से जुड़े issues कई सालों से खुले पड़े हैं, और हर बार नया data type जोड़ते समय compiler के साफ़ तौर पर धीमा होने का एहसास होता है

    • मुझे उत्सुकता है कि अक्सर वही जगहें Rust में फिर से लिखी जाती हैं जहाँ मूल कोड छोटा और सरल C utility जैसा था। 1 लाख लाइनों वाले बड़े C program के Rust port की तुलना में बहुत छोटे codebase ज़्यादा दिखते हैं। मैं जानना चाहता हूँ कि छोटे programs की compile speed में Rust और C की तुलना कैसी है। मेरा सवाल program के size का नहीं, compile speed का है। संदर्भ के लिए, हाल की माप के अनुसार Rust compiler toolchain का आकार मेरे इस्तेमाल वाले GCC से लगभग 2 गुना है। 1. इतने छोटे programs में किसी भी language में memory safety issue छिपे होने की संभावना कम होती है, और आकार छोटा होने से audit करना भी आसान होता है। यह 1 लाख लाइनों वाले C program जैसी स्थिति नहीं है
    • हर बार नया type define करने पर compiler के धीमा पड़ने का एहसास होता है। मेरी याद के अनुसार compiler performance type की “depth” के हिसाब से exponentionally धीमी हो सकती है। GraphQL जैसे मामलों में nested types बहुत होते हैं, इसलिए यह समस्या खास तौर पर गंभीर होती है
    • जब macro दर्जनों या सैकड़ों लाइनों तक expand होकर codebase को ज्यामितीय रूप से बड़ा कर देते हैं, तो इस समस्या से निपटने के लिए हाल ही में analysis tool support जोड़ी गई है। संबंधित सामग्री: https://nnethercote.github.io/2025/06/26/how-much-code-does-that-proc-macro-generate.html
  • Ryan Fleury ने Epic RAD Debugger को C में 2,78,000 लाइनों के unity build तरीके से बनाया है, जिसमें सारा code एक ही file में एक single compilation unit के रूप में जाता है, और Windows पर clean compile में सिर्फ 1.5 सेकंड लगते हैं। केवल इस उदाहरण से ही दिखता है कि compilation बेहद तेज़ हो सकती है; इसलिए उत्सुकता है कि Rust या Swift में ऐसा वैसा ही क्यों नहीं किया जा सकता

    • build time पर compiler जितना ज़्यादा काम करेगा, build time उतना लंबा होगा। Go बड़े codebase पर भी 1 सेकंड से कम build time हासिल कर सकता है। उसका कारण है build के लिए ज़रूरी काम को न्यूनतम रखने वाला सरल module system और type system, और अधिकतर चीज़ों को runtime GC पर छोड़ देना। इसके उलट, macro, जटिल type systems, और ऊँचे स्तर की robustness की माँग होने पर build time लंबी होना लगभग तय है
    • Rust में भी build unit पूरा crate होता है, और compiler उसे LLVM IR में उचित आकार के हिस्सों में बाँटता है। duplicate work और incremental build के बीच संतुलन भी वह खुद संभालता है। source code lines के हिसाब से Rust कई बार C++ से तेज़ build करता है। लेकिन Rust projects की खासियत यह भी है कि वे dependencies समेत सब कुछ compile करते हैं
    • Rust और Swift, C compiler से धीमे compile होते हैं क्योंकि language को स्वाभाविक रूप से कहीं ज़्यादा analysis की ज़रूरत होती है। उदाहरण के लिए Rust का borrow checker मुफ्त में नहीं आता। सिर्फ compile-time checks भी काफ़ी resources खा लेते हैं। C तेज़ है क्योंकि वह मूल syntax के आगे लगभग कुछ जाँचता ही नहीं। बल्कि C तो foo(char*) के लिए foo(int) जैसी अजीब calls भी नहीं पकड़ता
    • 2000 के दशक में मैंने कई दसियों हज़ार लाइनों वाले C++ projects compile किए थे, और पुराने computers पर भी build 1 सेकंड के भीतर खत्म हो जाती थी। दूसरी ओर, सिर्फ Boost इस्तेमाल करने वाला HELLO WORLD भी कई सेकंड ले लेता था। आख़िरकार build speed सिर्फ language या compiler पर नहीं, बल्कि code structure और इस्तेमाल किए गए features पर भी बहुत निर्भर करती है। C macro से DOOM भी बनाया जा सकता है, लेकिन शायद वह तेज़ नहीं होगा। दूसरी ओर Rust को भी तेज़ build के लिए structured किया जा सकता है
    • C और Go जैसी languages, जो तेज़ compilation को लक्ष्य बनाती हैं, उनका तेज़ होना कोई बहुत आश्चर्य की बात नहीं है। असली मुश्किल है Rust की semantics को तेज़ी से compile करना। यह सवाल Rust की official FAQ में भी है
  • मुझे बहुत अच्छा लगता है कि Go ने optimization से ज़्यादा compile speed को प्राथमिकता दी। server, networking, और glue code जैसे कामों में तेज़ compilation सबसे अहम होती है। मैं कुछ हद तक type safety भी चाहता हूँ, लेकिन इतनी नहीं कि loosely prototype करने में बाधा बने। GC होना भी सुविधाजनक है। मुझे लगता है Google ने बड़े पैमाने पर development के अनुभव के बाद यह निष्कर्ष निकाला कि सरल types, GC, और बेहद तेज़ compilation, runtime speed या semantic perfection से कहीं ज़्यादा महत्वपूर्ण हैं। Go में बने बड़े networking और infrastructure software के उदाहरण देखें तो यह चुनाव एकदम सही बैठता है। बेशक, जहाँ GC स्वीकार्य नहीं है या पूर्ण शुद्धता ज़्यादा महत्वपूर्ण है, वहाँ Go न चुना जाए; लेकिन मेरे काम के माहौल में Go के ये चुनाव आदर्श हैं

    • मुझे भी Go पसंद है, लेकिन मैं यह नहीं मानता कि यह language Google की किसी महान सामूहिक बुद्धि का परिणाम है। अगर उसमें Google का अनुभव सचमुच गहराई से समाया होता, तो मसलन null pointer exception को statically eliminate करने जैसी सुविधाएँ जोड़ी जातीं। बल्कि यह कुछ Google developers द्वारा अपनी पसंद की language बनाने जैसा ज़्यादा लगता है
    • Go के फायदे जैसे तेज़ compilation, ठीक-ठाक type system, और GC मौजूद हैं, लेकिन design space में Java पहले से कुछ ऐसा स्थान ले चुका था। मुझे लगता है Go बनने की बड़ी वजह सिर्फ सृजन की इच्छा थी, और अंततः अपने मूल target (server-side C/C++/Java) से ज़्यादा इसने scripting language (Python/Ruby/JS) उपयोगकर्ताओं को आकर्षित किया। scripting users को आसान और तेज़ type system चाहिए था, और Java बहुत पुराना और नीरस लगने लगा था। Java के लिए server/conference/library क्षेत्रों में अब वैसी जगह नहीं बची थी
    • यह कहानी भी है कि Google के developers ने C++ project के compile होने का इंतज़ार करते-करते Go design किया
    • मैं पूछना चाहता हूँ कि “obnoxious type” से मतलब क्या है। कोई type या तो data को सही तरह व्यक्त करता है या नहीं करता; और व्यवहार में किसी भी language में type checker को ज़बरदस्ती चुप कराया जा सकता है
    • Go अपने design goals और वास्तविक use case के लिए बिल्कुल सही language है। सबसे बड़ा जोखिम parallel processing और mutable state को channels के जरिए साझा करने वाली शैली में है; इस हिस्से में सूक्ष्म या नाज़ुक bugs पैदा हो सकते हैं। आमतौर पर ज़्यादातर उपयोगकर्ता यह pattern इस्तेमाल नहीं करते। मैं Rust इस्तेमाल करता हूँ, और मेरा काम ऐसी स्थिति का है जहाँ धीमे hardware पर धीमे algorithm को भी अधिकतम निचोड़कर चलाना पड़ता है। इसकी वजह से बड़े पैमाने की parallelization बहुत सूक्ष्म कारणों से लगभग असंभव समस्या बन जाती है
  • मुझे यह दावा समझ नहीं आता कि single static binary install करना container management से सरल है

    • लगता है कि लोग ठीक से नहीं समझते कि docker वास्तव में क्या करता है। उदाहरण के लिए, यह कहा गया कि “docker image से deploy करने पर हर बार सब कुछ नए सिरे से build करना पड़ता है,” लेकिन आंतरिक build/deploy वातावरण में यह समस्या ज़रूरी नहीं है। व्यक्तिगत उपयोग में तो local machine पर build की गई file को ही container में डाल सकते हैं और development convenience भी बनी रहती है। बस build environment के artifact paths जैसी चीज़ों का ध्यान रखना होता है। CI/CD या टीम projects में ज़ोर इस बात पर होता है कि कहीं से भी zero से build दोबारा बनाई जा सके, लेकिन निजी काम में यह हमेशा ज़रूरी नहीं होता
    • मूल लेख में लक्ष्य simplification नहीं, modernization है। मतलब, “पिछले 10 सालों में अधिकतर software ने container deployment को standard बना लिया है, इसलिए मैं भी अपनी website को docker, kubernetes जैसे container के रूप में deploy करूँगा।” container process isolation, security, standardized logging, horizontal scalability जैसी कई सुविधाएँ देते हैं
  • मेरे laptop (Mac M4 Pro) पर Deno (एक बड़ा Rust project) की पूरी compilation में 2 मिनट लगते हैं। command के हिसाब से debug लगभग 1 मिनट 54 सेकंड और release लगभग 8 मिनट 17 सेकंड लेता है। यह माप incremental compilation के बिना ली गई है। वैसे production/deployment build तो CI/CD system में चलती है, इसलिए मुझे खुद बैठकर इंतज़ार नहीं करना पड़ता

  • Cranelift की चर्चा कहाँ है? मेरी नज़र में Rust में game development करते हुए compile time इतना लंबा हो गया था कि मैं लगभग छोड़ने वाला था। जाँच करने पर पता चला कि LLVM optimization level से अलग भी धीमा है। Jai language के developers इस बात की ओर हमेशा इशारा करते रहे हैं। मैंने Cranelift के साथ build time को 16 सेकंड से 4 सेकंड तक गिरते देखा है। Cranelift team कमाल की है!

    • हाल की Bevy game jam में Dioxus community का बना subsecond नाम का tool इस्तेमाल किया था, और नाम की तरह ही इसने system hot reload को 1 सेकंड से कम कर दिया, जिससे UI prototyping में बहुत मदद मिली। https://github.com/TheBevyFlock/bevy_simple_subsecond_system
    • मेरा समझना है कि zig team भी LLVM के बिना अपना compiler backend बनाकर build time बहुत तेज़ करने की कोशिश कर रही है
    • मुझे लगता था कि पहले Cranelift में macOS aarch64 support नहीं था, लेकिन अब पता चला कि हाल में यह support आ गई है
    • 16 सेकंड के build time की वजह से Rust छोड़ने ही वाले थे, यह कुछ ज़्यादा नहीं है?
  • मुझे Rust धीमा नहीं लगता। अपनी श्रेणी की languages की तुलना में यह काफ़ी तेज़ है, और 15-15 मिनट लेने वाली C++/Scala compilation के मुकाबले तो बहुत तेज़ है

    • मैं भी सहमत हूँ। मुझे कभी Rust build कोई खास असुविधाजनक नहीं लगी। शायद शुरुआती दिनों की खराब छवि अब तक चली आ रही है, इसलिए ऐसी राय बन गई है
    • compile करते समय memory usage, C/C++ की तुलना में बहुत अधिक है। मुझे YouTube demo के लिए VM पर Rust का बड़ा project compile करना हो तो 8GB से ज़्यादा चाहिए होता है। C/C++ में ऐसी चिंता नहीं होती
    • C++ templates का Turing-complete होना देखते हुए, असली coding style को नज़रअंदाज़ कर सिर्फ build time की तुलना करना अर्थपूर्ण नहीं है
  • एक पुराने C++ developer के रूप में मुझे यह दावा ठीक से समझ नहीं आता कि Rust build धीमी है

    • शायद यही वजह है कि लोग कहते हैं Rust, C++ developers को target करती है। जिनके पास C++ का बहुत अनुभव है, वे toolchain की असुविधा को सहने वाले Stockholm syndrome के शिकार हो चुके होते हैं
    • C++ से तेज़ होने पर भी absolute terms में यह धीमी हो सकती है। C++ build की बदनामी तो सभी जानते हैं, वह कुख्यात रूप से बुरी है। Rust में वैसे structural language issues नहीं हैं, इसलिए उससे अपेक्षाएँ और बढ़ जाती हैं
    • मुझे यह classic उदाहरण लगता है कि नई सुविधाएँ तो लगातार जोड़ी जाती हैं, लेकिन असली users की बात सुनकर समस्याएँ हल करने पर उतना ध्यान नहीं दिया जाता
    • C की compilation stages कम और सरल थीं, इसलिए वह तेज़ थी; लेकिन C++ में templates के इस्तेमाल ने मुझे हमेशा ऐसा महसूस कराया कि उसने encapsulation के ज़्यादातर काम को तोड़ दिया। एक template header बदलो और लगता है जैसे पूरे project के 98% हिस्से पर असर पड़ गया
  • incremental compilation सच में बहुत ताकतवर है। शुरुआती build के बाद incremental cache snapshot को स्थिर कर दिया जाए, तो अगर बदलाव न हों, वही चीज़ बहुत तेज़ी से build/deploy में इस्तेमाल हो सकती है। docker के साथ भी इसकी अच्छी जोड़ी बनती है। compiler version या बड़े website updates को छोड़ दें तो image build layers को छूने की ज़रूरत नहीं पड़ती। अगर सिर्फ code बदला है, तो layers को इस तरह सेट किया जा सकता है कि बेकार में rebuild न हों, और यह काफ़ी efficient है

    • मेरे project के incremental artifacts 150GB से भी ज़्यादा हैं। docker image को इतना बड़ा रखने की कोशिश में व्यवहारिक रूप से बहुत गंभीर समस्याएँ आ गई थीं
  • मेरी homepage का build time 73ms है। static site generator 17ms में फिर से compile हो जाता है। असल generator run भी सिर्फ 56ms लेता है। zig build log का output भी संलग्न है

    • C/C++ में हमेशा Rust अच्छा है वाली टिप्पणियाँ आती हैं, और Rust में हमेशा Zig अच्छा है वाली। (बाद में पता चला कि यह टिप्पणी zig के मुख्य developer ने लिखी थी।) मुझे लगता है language evangelism community के लिए हानिकारक है; यह नए users लाने से ज़्यादा उल्टा प्रतिरोध पैदा करती है। अगर कोई सचमुच अपनी language से प्यार करता है, तो ऐसी प्रचार संस्कृति को कम करना ही मददगार होगा
    • सिर्फ एक compilation time number देने के बजाय, अच्छा होता अगर मूल लेख के विषय पर कोई सीधी चर्चा या व्याख्या भी होती
    • मेरी Rust website भी (जिसमें react-like framework और असली webserver शामिल है) cargo watch के साथ incremental build में लगभग 1.25 सेकंड लेती है। subsecond[0] जैसी incremental linking और hotpatching चीज़ें जोड़ें तो यह और तेज़ हो जाती है। Zig जितनी नहीं, लेकिन काफ़ी करीब है। अगर ऊपर बताई गई 331ms clean build थी, यानी बिना cache के, तो वह मेरी website की 12 सेकंड clean build से कहीं तेज़ है। [0]: https://news.ycombinator.com/item?id=44369642
    • मैं @AndyKelley से ज़रूर पूछना चाहूँगा कि उनके हिसाब से zig की compilation इतनी तेज़ और Rust/Swift लगातार धीमे होने की निर्णायक वजह क्या है
    • Zig memory safety की guarantee नहीं देता, सही?