1 पॉइंट द्वारा GN⁺ 2025-06-08 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • लो-लेवल ऑप्टिमाइज़ेशन वह काम है जिससे compiler code के इरादे और constraints को बेहतर समझ पाता है, और Zig इस उद्देश्य के लिए अच्छी तरह फिट बैठता है क्योंकि इसमें type, alignment, aliasing और compile-time जानकारी को स्पष्ट करना आसान है
  • LLVM जैसे optimizing compiler भी हमेशा सर्वश्रेष्ठ code नहीं बनाते, इसलिए bottleneck वाले हिस्सों में generated code की जाँच और code adjustment अब भी ज़रूरी है
  • Zig में noalias, align, fixed array size, और element type को compile time पर पास करके JavaScript example की तुलना में छोटा vectorized code बनाया जा सकता है
  • comptime सामान्य Zig code को compile time पर चलाकर constants generation, generics implementation, type reflection, और string comparison optimization जैसी metaprogramming को संभव बनाता है
  • Zig की ताकत AST को सीधे बदलने वाले macro की बजाय भाषा में integrated compile-time execution में है, और कुछ runtime values को भी compile-time specialization functions के जरिए dispatch किया जा सकता है

Compiler पर भरोसा करें, लेकिन verify करना क्यों ज़रूरी है

  • Optimization सिर्फ़ तेज़ program बनाने की technique नहीं है; यह cost reduction, बेहतर scalability, और system simplicity बनाए रखने से भी जुड़ी है
  • Modern compilers LLVM जैसे backend के साथ impressive results देते हैं, लेकिन कुछ स्थितियों में वे अब भी suboptimal code generate करते हैं
  • Low-level languages तेज़ इसलिए नहीं होतीं कि उनमें garbage collection या interpreter overhead कम होता है, बल्कि इसलिए भी कि वे compiler के समझने लायक intent information ज़्यादा express कर सकती हैं
  • Compiler algorithms या programming paradigm को खुद नहीं बदल सकता, और आम तौर पर loop जैसे सीमित दायरे में optimization करता है

JavaScript और Zig में array maximum का example

  • JavaScript example में x[i] = y[i] > x[i] ? y[i] : x[i] के रूप में दो arrays के element-wise maximum को x में store किया जाता है
  • इंसानों के लिए यह code स्पष्ट है, लेकिन V8 द्वारा generated bytecode फूला हुआ है
  • Zig example function arguments में optimization के लिए ज़रूरी जानकारी को अधिक specific रूप से बताता है
    • noalias x: x किसी दूसरे pointer का alias नहीं है
    • *align(64): 64-byte alignment
    • [65536]f64: array size और element type
    • const: read-only argument
  • इस जानकारी की वजह से compiler बेहतर code बना सकता है, और example में vectorized assembly generate होती है
  • बराबर Rust code भी लगभग वही assembly generate करता है

Optimization में Zig के फायदे और सीमाएँ

  • Zig verbose expressions की अनुमति देता है, जिससे LLVM को code information काफ़ी मात्रा में दी जा सकती है
  • Optimization से संबंधित Zig द्वारा दिए जाने वाले मुख्य elements ये हैं
  • Rust का memory model compiler को यह हमेशा मानने देता है कि function arguments aliases नहीं बनाते, लेकिन Zig में इसे खुद specify करना पड़ता है
  • अगर compiler को Zig function arguments के no-alias होने की जानकारी नहीं मिलती, तो annotation रहित Zig function Rust function से धीमा हो सकता है
  • अच्छी तरह annotated LLVM IR को ही मानक मानें, तब भी Zig अच्छे results देता है, लेकिन इसकी बड़ी ताकत compile-time execution में है

comptime की भूमिका

  • Zig का comptime compile time पर code generation के लिए feature है
  • Compile time पर किए जा सकने वाले काम ये हैं
    • constants generate करके binary में शामिल करना
    • कई data types के लिए समान hashmap structure को बार-बार न लिखना
    • compile time पर ज्ञात data के आधार पर अनावश्यक code हटाने के लिए optimization को guide करना
    • types को inspect, reflect, और generate करके generics implement करना
  • comptime code सामान्य Zig code है जो compile time पर execute होता है, और इसमें network IO जैसे side effects नहीं हो सकते
  • Compile-time emulation machine compile target से match करती है
  • लगभग सारा Zig code comptime के जरिए compile time पर execute किया जा सकता है, और सभी types को compile time पर inspect, reflect, और generate किया जा सकता है

Macro से फर्क

  • comptime का उद्देश्य macros जैसा है, लेकिन इसका काम करने का तरीका अलग है
  • कुछ macros raw text बदलते हैं, और कुछ program के AST को सीधे modify करते हैं
  • Zig का comptime AST को सीधे change नहीं करता, और इसमें token-pasting macro जैसी functionality भी नहीं है
  • Zig का लक्ष्य readable language होना है, इसलिए असंबंधित scopes में variables बनाने या modify करने वाली macro style से यह मेल नहीं खाता
  • Macros जो कर सकते हैं, लेकिन Zig comptime सीधे नहीं कर सकता, वे काम ये हैं
    • दूसरे macros define करना
    • AST बदलना
    • mini-language या DSL को सीधे implement करना
  • हालांकि Zig में भी DSL बनाया जा सकता है, और Zig का print function comptime से format string parse करके data serialize करने वाले function graph को construct करता है
  • उदाहरण के तौर पर TigerBeetle account testing DSL, comath, zilliam हैं

comptime string comparison optimization

  • सामान्य string comparison में length अलग होने पर false लौटाया जाता है, और length समान होने पर हर byte को क्रम से compare किया जाता है
  • इस तरीके में दोनों strings से अलग-अलग byte read करके compare करना पड़ता है
  • अक्सर एक string compile time पर पहले से ज्ञात होती है, इसलिए Zig में एक argument को comptime होना require किया जा सकता है
    • fn staticEql(comptime a: []const u8, b: []const u8) bool
  • "Hello!\n" जैसी static string से compare करते समय compiler length comparison और हर byte के constant comparison से बना code generate करता है
  • इस section का उद्देश्य सिर्फ़ यह दिखाना नहीं है कि compiler अपने-आप कौन-सा optimization कर सकता है, बल्कि यह दिखाना है कि comptime से transformation को force करके compiler को न दिखने वाले अवसर खोले जा सकते हैं

बड़े unit comparison और SIMD का उपयोग

  • सरल comptime string comparison अब भी byte-level comparison करता है
  • बेहतर version std.simd.suggestVectorLength(u8) या @sizeOf(usize) का उपयोग करके comparison block size तय करता है
  • पहले string length verify करने के बाद, compare किए जा सकने वाले बड़े blocks की संख्या और remaining bytes की संख्या calculate की जाती है
  • हर block को std.meta.Int(.unsigned, block_len * 8) से बने integer type में @bitCast करके compare किया जाता है
  • बचे हुए bytes को भी अलग integer type से compare किया जाता है
  • "Hello, World!\n" example की generated assembly बड़े registers का उपयोग करती है और conditional branches की संख्या घटाती है
  • बड़े string comparison में बड़े SIMD registers का उपयोग करने वाली assembly generate होती है

Runtime values और compile-time specialization को साथ इस्तेमाल करना

  • Zig का comptime सिर्फ़ compile time पर ज्ञात data तक सीमित नहीं है
  • सरल cases में compile time पर कई procedures generate कर दिए जाते हैं, और runtime value के अनुसार सही procedure पर dynamic dispatch किया जा सकता है
  • Example code में switch (runtime_val) में inline 0...100 range की values को staticFn(comptime_val) में भेजा जाता है, और बाकी को runtimeFn(runtime_val) से handle किया जाता है
  • अगर binary size में वृद्धि नहीं चाहिए, तो full runtime implementation पर fallback किया जा सकता है

निष्कर्ष

  • Zig का comptime templates, macros, generics, और manual code generation का विकल्प निभाता है
  • दूसरी languages में भी ऐसा ही काम किया जा सकता है, लेकिन Zig में comptime language में अधिक स्वाभाविक रूप से integrated है
  • Zig वास्तविक उपयोगी परिस्थितियों में high-performance code लिखना आसान बनाता है, और यह उस Turing tar-pit से उलट है जहाँ सब कुछ संभव होता है लेकिन दिलचस्प काम कठिन होते हैं
  • Language wars पर, एक ओर यह व्यापक दृष्टिकोण बचा रहता है कि Turing completeness ही पर्याप्त है, और साथ ही यह रुख भी कि लोगों की पसंदीदा languages हो सकती हैं
  • “C Python से तेज़ है” जैसे भाषा को ही benchmark target मानने वाले कथन गलत हो सकते हैं; असली benchmark target भाषा नहीं, बल्कि specific code और implementation होता है

1 टिप्पणियां

 
GN⁺ 2025-06-08
Hacker News की टिप्पणियाँ
  • Zig में जो बात सबसे ज़्यादा आकर्षित करती है, वह है build system की आसानी, cross compile करना, और तेज़ iteration speed को लक्ष्य बनाना
    गेम डेवलपर होने के कारण performance की ज़रूरत तो है, लेकिन मुझे लगता है कि ज़्यादातर भाषाएँ ज़रूरी performance पर्याप्त रूप से दे देती हैं, इसलिए भाषा चुनने में यह मेरी पहली प्राथमिकता नहीं है
    किसी भी भाषा में दमदार code लिखा जा सकता है, लेकिन असली बात है ऐसा future-proof framework चुनना जो दशकों तक modular code को maintain कर सके
    C/C++ इसलिए default जवाब था क्योंकि इसे हर जगह support मिलता है, और लगता है कि Zig भी उस स्तर तक पहुँच सकता है

    • मुझे Zig पसंद है, लेकिन long-term maintainability और modularity को मैं उलटे इसकी सबसे कमजोर बातों में से एक मानता हूँ
      Zig encapsulation के खिलाफ-सा है, और struct members को private नहीं बनाया जा सकता: https://github.com/ziglang/zig/issues/9909#issuecomment-9426...
      मुख्य उद्धरण का रुख यह है कि “private fields और getter/setter Java द्वारा popular किया गया anti-pattern हैं; fields मौजूद data हैं, इसलिए उन्हें सावधानी से नाम दें और public API के हिस्से के रूप में document करें”
      अगर internal representation को छिपाया नहीं जा सकता, तो software modularity की बुनियाद यानी API contract ठीक से बनाना मुश्किल है, और user code तोड़े बिना internal representation बदल पाने की क्षमता होनी चाहिए
      Zig का रुख यह दिखता है कि internal representation नाम की कोई अलग चीज़ नहीं होनी चाहिए, बल्कि representation को ही public, documented और guaranteed होना चाहिए; उम्मीद है कि कभी वे यह फैसला पलटकर private fields support करेंगे
    • मज़े के लिए एक पुराने Kindle device पर कट-छँट किए गए Linux 4.1.15 के ऊपर Zig चलाकर देखा; अनुभव काफी दिलचस्प था और Zig की maturity देखकर pleasantly surprised हुआ
      बहुत-सी चीज़ें सीधे चल गईं, और पुराने GDB से अजीब bugs तक debug कर पाया
      मैं भी Zig से convince हो गया, और इसके बारे में यहाँ लिखा: https://news.ycombinator.com/item?id=44211041
    • Rust थोड़ा इस्तेमाल किया था और पसंद आया था, लेकिन इसकी खराब reputation की बातें सुनकर कुछ समय रुक गया; फिर दोबारा इस्तेमाल किया तो अब भी पसंद आया
      समझ नहीं आता लोग इससे इतनी नफ़रत क्यों करते हैं
      देखने में खराब लगने वाले generics C# या TypeScript में भी हैं, और borrow checker अगर आपने low-level काम किया है तो समझ में आने वाला concept है
    • Zig एक सरलतर Rust और बेहतर Go जैसा दिखता है
      Zig पर बने tools में जो सचमुच प्रभावित करता है वह bun है, और bun इस्तेमाल करने के बाद ज़िंदगी काफी सरल हो गई
      Rust में बने uv के बारे में भी कुछ ऐसा ही कहा जा सकता है
    • Console पर Zig कैसे चलेगा, यह जानने की उत्सुकता है
      आम तौर पर consoles C/C++ के अलावा चीज़ें पसंद नहीं करते, लेकिन Zig को C में translate किया जा सकता है, इसलिए शायद इसे पूरी तरह बाहर न रखा जाए
  • “नए compilers भी language spec तोड़ते हैं (Clang मानता है कि बिना side effects वाले सभी loops terminate होते हैं)” इस दावे पर: मुझे शक नहीं कि compilers कभी-कभी spec तोड़ते हैं, लेकिन इस मामले में Clang कम-से-कम C11 के बाद के मानक के हिसाब से सही है
    C11 में कहा गया है कि जिन loops की controlling expression constant expression नहीं है, और जो input/output, volatile access, synchronization या atomic operations नहीं करते, उनके बारे में implementation यह मान सकता है कि वे terminate होंगे

    • C++ में C++26 आने तक सभी loops के बारे में ऐसा कहा जाता है, लेकिन जैसा आपने बताया, C में खुद ऐसा नहीं है; वहाँ सिर्फ “controlling expression constant expression नहीं है” वाले मामले पर लागू होता है
      इसलिए C का साधारण infinite loop for (;;); सचमुच infinite loop के रूप में compile होना चाहिए, और Rust का कम opaque loop {} भी ऐसा ही होना चाहिए
      लेकिन LLVM ऐसे लोगों द्वारा बनाया गया है जो कभी-कभी भूल जाते हैं कि वे हमेशा C++ compiler नहीं बना रहे; इसलिए एक समय ऐसा था जब Rust ने “मुझे infinite loop दीजिए” कहा तो LLVM ने “C++ के हिसाब से ऐसी कोई चीज़ नहीं होती, इसलिए optimize कर दूँगा” कह दिया, और यह दूसरी भाषा पर गलत तरह से लागू करना था
  • string comparison को inline और unroll करने के लिए comptime अनिवार्य नहीं है
    C में भी यह संभव है: https://godbolt.org/z/6edWbqnfT
    typo ठीक कर दिया है

    • सही बात है, और पहला उदाहरण बहुत simple था
      बेहतर उदाहरण है https://github.com/RetroDev256/comptime_suffix_automaton
      हालांकि link किए गए godbolt code में सच में दो खराब examples में से एक दिख भी रहा है
  • JavaScript उदाहरण और Zig/Rust उदाहरणों की तुलना करना मुझे अच्छी तुलना नहीं लगता
    Zig और Rust compiler को तो बहुत आधुनिक target CPU चुनने को कहा गया है, लेकिन V8 के लिए वही शर्तें नहीं लगतीं
    optimizing JIT भी, अगर conditions सही हों, तो vectorization कर सकता है
    संदर्भ के लिए, ज़्यादातर modern languages strings पर वही optimization करती हैं, और C++ example यहाँ है: https://godbolt.org/z/TM5qdbTqh

    • कुल मिलाकर यह सेब और fruit salad की तुलना जैसा लगता है, लेकिन JS और Zig के use cases का फर्क दिखाने के लिए ठीक है
      Zig example fixed-size के known type arrays का इस्तेमाल करता है, जबकि JS code runtime पर “generic” है, इसलिए x और y कोई भी object हो सकते हैं
      JS में वह cost चुकानी पड़ती है, यह सही है, लेकिन विडंबना यह है कि इस specific example में JIT को type information और बेहतर तरीके से दी जा सकती है
      अगर इस function को हमेशा same size की Float64Array के साथ call कराया जाए, तो JIT इसे जानकर ज्यादा तेज loop बना सकता है। vectorization न भी हो, तब भी यह काफी बेहतर होगा
      हालांकि typed arrays की initialization cost ज्यादा होती है, इसलिए जब तक कोई बड़ा typed array एक बार allocate करके बहुत बार reuse न किया जाए, उन्हें असल में ज्यादा नहीं इस्तेमाल किया जाता
      साथ ही लेख में कहा गया कि JS bytecode काफी फूल गया था; शायद इसका बड़ा हिस्सा guards जोड़ने से आता होगा, क्योंकि JIT यह guarantee नहीं कर सकता कि 65536 दोनों arrays की length के बराबर है
      फिर भी असल में कोई भी for loop इस तरह नहीं लिखेगा, बल्कि i < x.length लिखेगा, और इस case में JIT कम-से-कम एक array check तो हटा देता है
    • Rust और Zig के godbolt examples में target को किसी पुराने CPU पर बदला जा सकता है
      JS target की limitations के बारे में न सोच पाने के लिए माफ़ी
      linked C++ example यह दिखाने का अच्छा example है कि Clang C++ में क्या कर सकता है, लेकिन यह ध्यान में रखते हुए भी कि Zig किसी specific CPU के लिए compile करता है, generated assembly थोड़ी निराशाजनक लगती है
      https://github.com/RetroDev256/comptime_suffix_automaton का C++ port देखना बहुत दिलचस्प होगा
      यह comptime का ऐसा use है जिसे C++ compiler साफ़-साफ़ infer नहीं कर सकता
  • “high-level languages में low-level languages में मौजूद समृद्ध intent की कमी होती है” यह वाक्य सच में सही है या नहीं, पता नहीं
    intent expression high-level/low-level spectrum का factor नहीं लगता; बल्कि जितने ज्यादा तरीके intent को अधिक detail में express करने के हों, उतना ही वह high-level के करीब होना चाहिए

    • सहमत हूँ, और आगे बढ़कर कहूँ तो high-level language और low-level language का मूल फर्क यह है कि high-level language में intent express किया जाता है, जबकि low-level language में underlying mechanism express करना ही पड़ता है
    • यहाँ जिस intent की बात है, वह “इस purchase का tax rate calculate करो” नहीं, बल्कि “इस byte को left में तीन positions shift करो” के ज्यादा करीब लगता है
      यह इस बारे में intent है कि machine से क्या करवाना है, न कि क्या हासिल करना है
      purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?; जैसा code intent से भरा है, लेकिन आखिर में कौन-सा machine code निकलेगा, यह पता नहीं चलता
  • वह for loop syntax भयानक है
    क्या मतलब है कि दो lists साथ-साथ हैं, और एक list में item की position दूसरी list में item की position से correspond करती है?
    देखते ही आँखें दुखने लगती हैं
    लगता है modern languages गलत रास्ते पर चली गई हैं, जब उन्होंने parser में तरह-तरह का “magic” जोड़ना और code में जगह-जगह छोटे symbols बिखेरना शुरू किया
    यह ऐसा रूप नहीं है जिसे घंटों तक देखते रहने का मन करे

    • ऐसे arrays language से परे low-level code में बहुत common pattern हैं, और उन्हें parallel में iterate करना भी वैसा ही है
      इसलिए Zig का ऐसा syntax देना स्वाभाविक है जो ठीक यही काम आसान बनाता है और साथ ही साफ़ दिखाता है कि क्या हो रहा है
      व्यक्तिगत रूप से मुझे यह काफी अच्छा लगा; उत्सुक हूँ कि इसमें आँखें क्यों दुखती हैं
  • Zig का allocator model मुझे सच में पसंद है
    अच्छा होता अगर Go में भी garbage collection के बजाय request-level allocator जैसी चीज़ इस्तेमाल की जा सकती

    • Go में भी custom allocators और arenas संभव हैं और वास्तव में मौजूद भी हैं, लेकिन उनकी usability बहुत खराब है और सही तरीके से इस्तेमाल करना मुश्किल है
      भाषा में ही ownership rules express और enforce करने का तरीका नहीं है, इसलिए अंत में यह बस थोड़े अलग syntax वाला C लिखकर सब ठीक रहने की उम्मीद करने जैसा बन जाता है
      garbage collection न होने की शर्त में C++ भी Go से कहीं ज्यादा safe है
  • Rust का memory model compiler को यह हमेशा मानने देता है कि function arguments कभी aliasing references नहीं करते, लेकिन Zig में इसे manually specify करना पड़ता है
    इस तरह की aliasing specification से बचा जा रहा है
    क्योंकि इसे समझने वाले कम हैं, और गलत इस्तेमाल करने पर code में ऐसे bugs आ सकते हैं जिन्हें समझना कठिन होता है

  • इस दावे के बारे में कि Zig की comptime flexibility ने दूसरी languages में अच्छे improvements लाए, compile-time function execution और constant arguments लेने वाले functions D ने 2007 में introduce किए थे और कई languages को कुछ ऐसा ही अपनाने के लिए प्रेरित किया था
    https://dlang.org/spec/function.html#interpretation

  • मुझे Zig पसंद है, लेकिन “मुझे Zig की verbosity पसंद है” कहना अजीब लगता है
    यह सही है कि C कई जगहों पर बहुत ढीला है, लेकिन मौजूदा मानकों से Zig उल्टी दिशा में थोड़ा ज़्यादा झूल गया है, जिससे कई बार comment जैसी type annotation noise बहुत बढ़ जाती है
    खासकर expressions में explicit integer casting के मामले में ऐसा है, और इस पर मैंने यहाँ थोड़ा लिखा है: https://floooh.github.io/2024/08/24/zig-and-emulators.html
    performance के लिहाज से अगर Zig code समान C code से तेज़ है, तो आम तौर पर वजह Zig की ज़्यादा aggressive LLVM optimization settings होती हैं
    उदाहरण के लिए Zig default रूप से -march=native और whole-program optimization का उपयोग करता है, और project के सारे Zig code को एक ही compilation unit के रूप में compile करता है
    unreachable को optimization hint की तरह इस्तेमाल करने जैसे लगभग सभी “tricks” C में भी संभव हैं, बस कभी-कभी non-standard language extensions की ज़रूरत पड़ती है
    C compilers, खासकर Clang भी constant folding बहुत aggressive तरीके से करते हैं, और गहरी call stack होने पर भी constant में fold हो सकने वाले बड़े code regions को छोटा कर सकते हैं
    इसलिए code generation के लिहाज से कई बार Zig के comptime से बड़ा फर्क नहीं होता
    comptime का फायदा यह है कि वह चुपचाप runtime code में वापस नहीं गिरता, और non-comptime code भी C की तरह वही constant folding optimization का target बनता है
    जैसे, अगर कोई “pure” non-comptime function constant arguments के साथ call हो, तो compiler फिर भी function call को उसके result से replace कर देता है
    संक्षेप में, अगर C code Zig code से धीमा है, तो C compiler settings जांचनी चाहिए। आखिर optimization का सारा भारी काम नीचे LLVM में ही होता है

    • casting उदाहरण के संदर्भ में, cast को function में wrap किया जा सकता है
      fn signExtendCast(comptime T: type, x: anytype) T { const ST = std.meta.Int(.signed, @bitSizeOf(T)); const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x))); return @bitCast(@as(ST, @as(SX, @bitCast(x)))); }
      export fn addi8(addr: u16, offset: u8) u16 { return addr +% signExtendCast(u16, offset); }
      यह उसी assembly में compile होता है, reusable है, और intent भी साफ हो जाता है
    • Zig में दिलचस्प ideas हैं, और मुझे लगा था कि लेख low-level optimization पर ज़्यादा focus करेगा, लेकिन आखिर में बात “comptime और whole-program compilation शानदार हैं” वाली निकली
      मैं इससे सहमत हूँ
      Virgil में 2006 से पूरी language को compile time पर इस्तेमाल किया जा सकता था और वह whole-program compilation भी support करता था
      लेकिन Virgil LLVM को target नहीं करता, इसलिए speed comparison आखिरकार दो compiler backends की तुलना बन जाता है
      Virgil इस compilation model से संभव होने वाली reachability analysis और specialization optimizations पर काफी निर्भर करता है
      उदाहरण के लिए, यह method calls को aggressive तरीके से devirtualize करता है, unreachable fields और objects को हटाता है, fields और heap objects के जरिए constants को promote करता है, और polymorphic code को पूरी तरह monomorphize करता है
    • नया x86 backend आने पर शायद C और Zig के बीच performance difference में से वह हिस्सा दिख सके जो सिर्फ Zig project खुद के कारण है
    • explicit integer casting के संबंध में लगता है जल्द ही कुछ cleanup आने वाला है: https://ziggit.dev/t/short-math-notation-casting-clarity-of-...
    • आने वाले कुछ वर्षों में languages का overall अतिरिक्त झुकाव ज़्यादा verbose और explicit languages की तरफ काफी मजबूती से बढ़ेगा
      वजह सिर्फ यह है कि इससे AI के लिए उन्हें संभालना आसान हो जाता है
      coding में AI का उपयोग करना अच्छा idea है या नहीं, और किस हद तक अच्छा है, यह अलग मुद्दा है, लेकिन कई developers ऐसा मानते हैं और languages उन्हें accommodate करने की कोशिश करेंगी