1 पॉइंट द्वारा GN⁺ 2023-07-10 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • .NET Framework 4.x में केवल "Hello World" आउटपुट करने वाली executable file को लक्ष्य बनाकर, सामान्य C# compile result को 4.6KB से manual PE construction के जरिए 834 bytes तक घटाया गया
  • प्रयोग में managed entry point, Windows पर चल सकने वाली single file, केवल BCL या सामान्य Windows installation libraries का उपयोग, और file के अंत में मौजूद zero bytes को छोड़ना जैसी शर्तें रखी गईं
  • size reduction nullable reference type annotations और अनावश्यक metadata हटाकर, और 64-bit PE पर switch करके imports और base relocations हटाने के तरीके से की गई
  • Console.WriteLine के बजाय P/Invoke से ucrtbase!puts को call करके UTF-16 #US stream हटाई गई, और ASCII string को PE header और पहले section के बीच के padding area में ले जाया गया
  • अंतिम executable file trailing zero को छोड़कर 834B(0x342), और ZIP compression पर 476B तक घट गई, जो .NET PE और CLR metadata के minimal construction limits दिखाती है

प्रयोग की शर्तें और baseline

  • लक्ष्य था .NET executable file में केवल जरूरी minimum bytes डालना ताकि CLR standard output पर "Hello World" print करे
  • प्रयोग के नियम इस प्रकार थे
    • application को C# या CIL में implement किया गया managed entry point execute करना होगा
    • target runtime .NET Framework 4.x.x है
    • third-party dependencies निषिद्ध हैं, और केवल BCL mscorlib या सामान्य Windows installation libraries की अनुमति है
    • PE file और CLR की alignment constraints के कारण Windows 10 या बाद में चल सकने वाली theoretical minimum .NET PE file 1KB से छोटी नहीं हो सकती, लेकिन file के अंत की trailing zero bytes को size calculation से बाहर रखा गया
  • सामान्य C# Hello World program को net472, C# 10, nullable enabled setting के साथ build करने पर executable file size 4.6KB होता है
  • अंतिम source code Full Source Code में है

C# compile result में घटाए जा सकने वाले हिस्से

  • C# 8.0 के nullable reference types custom attributes के रूप में executable file में statically शामिल होते हैं, और ये attributes size को काफी बढ़ाते हैं
  • .csproj में <Nullable>disable</Nullable> से nullable reference type checks बंद करने पर attributes हट जाते हैं, लेकिन PE file alignment के कारण कुल file size फिर भी 4.6KB रहता है
  • decompile result के अनुसार C# compiler nullable बंद होने पर भी अनावश्यक metadata बनाता रहता है
    • assembly file version, copyright information जैसे assembly attributes
    • Program class के अलावा छिपा हुआ <Module> type
  • extreme reduction के लिए C# compiler settings और खोजने के बजाय, AsmResolver से .NET executable file सीधे बनाने का फैसला किया गया
  • सीधे बनाए गए module में <Module> type में main function रखा गया, और CIL body में केवल Ldstr "Hello World", Console.WriteLine, Ret शामिल किए गए
  • इस तरीके से file size पहले की तुलना में लगभग आधा हो गया

64-bit PE से imports और base relocations हटाना

  • बनाई गई executable file में .text और .reloc दो sections हैं, और ImportsBase Relocations data directories बड़ी जगह लेती हैं
  • AnyCPU या 32-bit .NET executable file में unmanaged entry point को mscoree!_CorExeMain call करना पड़ता है, इसलिए imports directory चाहिए
  • default .NET executable file relocatable होती है, इसलिए इस imported function call के लिए base relocation भी साथ में चाहिए
  • base relocation default रूप से अलग section में जाती है, और प्रत्येक section कम से कम 0x200 byte units में align होता है, जिससे file कम से कम 1KB से अधिक फूल जाती है
  • 64-bit .NET executable file में unmanaged entry point की जरूरत नहीं होती, इसलिए नीचे की settings से दोनों directories और .reloc section हटाए जा सकते हैं
    • module.PEKind = OptionalHeaderMagic.PE64
    • module.MachineType = MachineType.Amd64
  • इस चरण में valid .NET PE file के theoretical minimum size 1KB तक पहुंचा गया

metadata names, GUID, public key token हटाना

  • इसके बाद size measurement Windows Explorer के file size से नहीं, बल्कि hex editor में आखिरी non-zero byte के बाद की position के आधार पर किया गया
  • 1KB PE file इस मानदंड पर पहले ही 991 bytes(0x3DF) तक घट चुकी थी
  • #Strings heap .NET binary में second largest metadata stream है, और table stream #~ द्वारा इस्तेमाल किए जाने वाले type और method names store करती है
  • runtime पर महत्वपूर्ण न होने वाले names को null set करने पर भी program सामान्य रूप से चलता है, और इससे 32 bytes घटे
  • #GUID stream Module table के Mvid द्वारा refer किए गए GUID को रखती है, लेकिन प्रयोग में version identification की जरूरत न होने से इसे हटाया गया
    • Mvid के 16 bytes हटाए गए
    • खाली #GUID stream पूरी और 16-byte header भी हटाया गया
    • कुल 32 bytes घटे
  • Console::WriteLine call के लिए refer किए गए mscorlib में आमतौर पर 8-byte public key token लगा होता है
  • public key token न होने पर CLR इस assembly token की authenticity verify नहीं करता, और प्रयोग में इसे हटाया गया
  • इस चरण के बाद file size 918 bytes हो गया

Console.WriteLine हटाना और ASCII string इस्तेमाल करना

  • "Hello World" .NET के #US metadata stream में store होता है, और length prefix वाले 16-bit character array तथा अतिरिक्त 0 byte के रूप में represent होता है
  • output की जाने वाली string के सभी characters के code point 0xFF से छोटे हैं, इसलिए उन्हें 1-byte ASCII में represent किया जा सकता है, लेकिन CLR से #US stream की string को ASCII के रूप में interpret कराने का कोई तरीका नहीं है
  • Console::WriteLine के बजाय unmanaged function ucrtbase!puts को P/Invoke से call किया गया
    • C# declaration form है [DllImport("ucrtbase")] static extern int puts(nint str);
    • puts zero-terminated ASCII string pointer को argument के रूप में लेता है
  • compile time पर string का runtime address जानने के लिए PE optional header के DllCharacteristics में DynamicBase flag बंद किया गया
    • fixed image base set किया गया
    • ASCII string को .text section में रखा गया
    • runtime address module_base_address + rva_ascii_string से calculate किया गया
  • CLR इस flag का सम्मान करे, इसके लिए .NET data directory का ILOnly flag भी unset किया गया
  • इस बदलाव से wide-character string, #US stream, System.Console::WriteLine reference हटे, और उनकी जगह puts definition व P/Invoke metadata जोड़ा गया
  • result size 889 bytes(0x379) हो गया

अंतिम micro-optimization

  • puts की standard definition output किए गए characters की संख्या int32 के रूप में return करती है, लेकिन program इस value का उपयोग नहीं करता
  • 64-bit PE में Microsoft के x64 calling convention के अनुसार return value .NET evaluation stack में नहीं, बल्कि RAX register में जाती है
  • P/Invoke definition को void return में बदलने से main method में return value हटाने के लिए pop instruction खत्म किया जा सकता है
  • PE header size 0x200 से छोटा होने के कारण header और पहले section के बीच padding data बनता है
  • Windows PE Loader PE header को readable memory में map करता है, और इस range में padding data भी शामिल होता है
  • ASCII string को इस padding area में ले जाने पर file 13 bytes और घट सकती है
  • Console::WriteLine अब refer नहीं किया जाता, इसलिए mscorlib reference भी हटाया गया
    • tables stream #~ की एक table कम हुई
    • #Strings stream से mscorlib name भी हट गया
  • .NET metadata directory का VersionString default रूप से "v4.0.30319" होता है, और 4-byte multiple में padded होकर कुल 12 bytes लेता है
  • इस string को v4.0. तक घटाने पर भी CLR 4.0 boot होता है और program चलता है, और padding के बाद कुल 4 bytes और घटे
  • trailing . जरूरी पाया गया, और उससे अधिक घटाने पर program ठीक से boot नहीं होता

अंतिम परिणाम

  • अंतिम binary size trailing zero को छोड़कर 834 bytes(0x342) है
  • ZIP compression result 476 bytes है, जो 1KB limit तक पहुंचने के बाद extra optimization न करने पर मिलने वाले 582 bytes से छोटा है
  • प्रयोग C# compiler द्वारा बनाए गए 4.6KB Hello World से शुरू होकर, सीधे constructed PE file 834B तक घटा
  • अंतिम program अब भी सामान्य रूप से "Hello World" output करता है
  • यह काम practical उपयोगिता से ज्यादा .NET module file format और CLR metadata की minimum construction limits को explore करने वाला प्रयोग है

1 टिप्पणियां

 
GN⁺ 2023-07-10
Hacker News पर टिप्पणियां
  • न्यूनतम runnable program जैसी experiments वाकई अच्छी होती हैं
    Michal Strehovský ने उस framework का इस्तेमाल करके जिसे आज .NET के नाम से जाना जाता है, .NET runtime dependency के बिना 8KB से छोटे आकार का Snake game बनाया था
    Medium खास नहीं है और अब Twitter भी उपयोगी नहीं रहा, लेकिन मोटे तौर पर याद है कि "hello world" Windows 3.11 को भी support करता था
    https://medium.com/@MStrehovsky/building-a-self-contained-ga... [https://web.archive.org/web/20200103110836/https://medium.co... | https://archive.ph/b6qXE]
    https://news.ycombinator.com/item?id=22010159 (2020)
    https://news.ycombinator.com/item?id=22104734
    ताजा काम और sponsorship https://flattened.net पर देखे जा सकते हैं: "bflat - C# as you know it but with Go-inspired tooling"

    • "Go-inspired" C# की बात आगे बढ़ाएं तो, .NET में System.Threading.Channels है
      https://learn.microsoft.com/en-us/dotnet/core/extensions/cha...
    • अगर C# apps इतने छोटे हो सकते हैं कि user को download time तुरंत जैसा लगे, तो सोचता हूं कि क्या इसका इस्तेमाल C# WebAssembly को ज्यादा practical बनाने में किया जा सकता है
      याद पड़ता है कि download size का काफी बड़ा हिस्सा खुद .NET libraries थीं
      यह भी सोचता हूं कि क्या browser को target करने वाला और C# इस्तेमाल करते हुए छोटे binaries बनाने वाला कोई 2D game framework/engine है
    • Michal और CoreRT team की वजह से मैंने C# को फिर से देखना शुरू किया, और आज अगर NativeAOT न होता, तो खासकर Go मौजूद होने की स्थिति में, मैं शायद C# पर कभी विचार नहीं करता
  • यह लेख भ्रम पैदा कर सकता है। इस executable को system में installed runtime चाहिए, और असल में 2023 में .NET Framework 4.7.2 इस्तेमाल करता है
    इस तरीके से fair comparison करना हो तो शायद .jar या .py file ज्यादा सही रहेगी
    असली executable के मानक से देखें तो Windows, Linux, macOS पर चल सकने वाला Native AOT binary ज्यादा उपयुक्त है
    हालांकि GC, ThreadPool, Console जैसे सहायक code भी शामिल करने पड़ते हैं, इसलिए इसका काफी बड़ा होना स्वाभाविक है

    • जरूरी नहीं। असल output exe, main call के अलावा .NET Framework का लगभग उपयोग नहीं करता
      console पर bytes output करने की logic ucrt.dll द्वारा expose किए गए unmanaged code पर pinvoke call से संभाली जाती है
      दूसरे comment में share किया गया Snake minimization लेख यही काम Windows में bundled न होने वाले नए .NET यानी .NET Core के आधार पर करता है, और वास्तव में non-.NET binary बनाता है
      [0]: https://medium.com/@MStrehovsky/building-a-self-contained-ga...
    • पूरी तरह statically linked native binary की तुलना ऐसे binary से करना भी काफी unfair है जो कुछ या सारी libraries dynamically load करता है
      container images की तुलना करना अच्छा starting point होगा। संभव हो तो सबसे छोटा base image इस्तेमाल करें, या संभव हो तो "FROM scratch" इस्तेमाल करें, लेकिन तुलना सिर्फ same architecture के बीच होनी चाहिए
      64-bit पर 32-bit चलाने या pointer compression जैसे variables हटाने के लिए 32-bit को prefer करूंगा
      उसके बाद uncompressed export tar size की तुलना करना अच्छा रहेगा। पूरी तरह fair न सही, पर obvious variables कम किए जा सकते हैं
      और सोचें तो container के अंदर compression को भी reflect करने के लिए maximum compressed image size की तुलना करना ज्यादा fair हो सकता है। बेशक वही algorithm और parameters इस्तेमाल करने होंगे, या compression benchmark की तरह decompressor size को final result में जोड़ना होगा
    • मेरे hello world को "bash" framework चाहिए और size 0 bytes है
      इसे ऐसे call करें: echo "hello world"
    • .NET Framework Windows में built-in है, और कम से कम Vista के बाद से required component रहा है
      खास तौर पर .NET Framework 4.7.2 supported Windows versions में default रूप से शामिल होगा
    • मुझे नहीं पता कि हमें "fairness" की चिंता करनी चाहिए या "reality" की
      modern Windows install में .NET न हो, यह संभव नहीं लगता, और Linux तथा macOS में यह default रूप से नहीं होता; इसे fairness का आधार बनाना अजीब लगता है
  • वैसे, मुझे उत्सुकता है कि क्या किसी ने देखा कि लेखक की लोकेशन Antarctica McMurdo Station है
    मुझे अंदाज़ा नहीं था कि उस स्टेशन पर इस तरह के tech stack वाला कोई होगा: https://usscar.org/directory?combine=&field_usscar_nsf_progr...
    यह काफ़ी दिलचस्प कहानी बन सकती है

    • मैं एक research trip पर McMurdo गया था
      बाहरी लोगों को जो बात चौंकाती है, वह यह है कि वहाँ ज़्यादातर लोग support staff हैं, और वैज्ञानिक एक छोटा हिस्सा हैं, शायद लगभग 20% ही
      support staff में firefighters, carpenters, cooks, mechanics आदि—एक आधुनिक शहर चलाने के लिए ज़रूरी हर तरह के पेशे—शामिल होते हैं
      शायद selection process की वजह से, लगभग सभी बेहद काबिल होते हैं और अपने क्षेत्र में शीर्ष स्तर के होते हैं। व्यापक skills वाले Swiss Army knife जैसे लोग आश्चर्यजनक रूप से बहुत थे
      वहाँ रहने और काम करने की कठिनाइयाँ अनोखे और बहुत motivated लोगों को रोशनी की ओर खिंचे कीड़ों की तरह आकर्षित करती हैं, और कई लोग साल-दर-साल लौटते हैं। शानदार community है
    • इस मामले में “शनिवार को मूर्खतापूर्ण ढंग से खर्च किया” वाली लाइन समझ नहीं आती
      वहाँ और क्या कर सकते हैं?
    • https://brr.fyi/ देखना अच्छा रहेगा
    • उत्सुक हूँ कि यह कैसे खोजा
      .NET metadata या about में तो नहीं दिखता
  • कहा गया कि सभी sections को न्यूनतम संभव section alignment value, 0x200 bytes, पर align करना पड़ता है, इसलिए file कम से कम उतनी तो फूल जाती है, लेकिन 0x200 का मतलब 512 bytes है, यानी 0.5KB
    इस स्तर का PE size optimization मैंने बहुत पहले किया था, लेकिन याद है कि Windows 9x में 512 minimum था और NT में 1 तक छोटी alignment भी स्वीकार हो जाती थी
    DOS MZ और PE headers को overlap करने वाली वह trick भी नहीं दिखी जो पहले state-of-the-art हुआ करती थी: http://www.phreedom.org/research/tinype/
    और demoscene में पता चलता है कि वे यह काम 1KB binary के अंदर कर लेते हैं:
    https://www.pouet.net/prodlist.php?type%5B%5D=1k&platform%5B...

  • यह समय का मूर्खतापूर्ण उपयोग बिल्कुल नहीं है
    binary की बारीकियों में उतरने से उसके बनने के तरीके पर गहरी insight मिलती है, और वहाँ सीखे concepts आपके ज्ञान को समृद्ध करते हैं
    अगर यह दिलचस्प है और आप कुछ सीख रहे हैं, तो इसमें समय और मेहनत लगाना हमेशा worthwhile है

  • यह ज़्यादा relevant और दिलचस्प है: https://twitter.com/MStrehovsky/status/1669502394827419648
    इसमें कहा गया है कि “GC वगैरह सब कुछ सहित, पूरी तरह self-contained, native-compiled C# Hello World लगभग 440KB तक छोटा हो सकता है”
    .NET classic framework Windows की अब recommend न की जाने वाली legacy चीज़ है, और नए apps उस पर आधारित नहीं बनाने चाहिए

    • Resulting exe असल में framework को “use” नहीं करता; वह सिर्फ unmanaged pinvoke call के रूप में expose की गई lower ucrt.dll library की implementation detail पर निर्भर करता है
    • Office/COM API पर निर्भर app बनाना अपवाद हो सकता है
  • उम्मीद थी कि बात “वास्तविक” binary की होगी, यानी .NET AOT जैसे natively compiled .NET executable की
    https://learn.microsoft.com/en-us/dotnet/core/deploying/nati...

    • अभी कुछ MB के .NET AOT examples बनाए जा सकते हैं
      सबसे ज़्यादा shrink किए गए AOT compiled builds जल्द ही 1MB barrier तोड़ते दिखते हैं:
      https://github.com/AustinWise/SmallestDotnetHelloWorlds
  • बधाई। आपने Unicode support छोड़कर puts के लिए C call बना दी
    क्या इसे सीधे C compiler से compile करना आसान नहीं होता?
    thats_just_c_with_extra_steps.jpg

    • compile को practically skip कर देना थोड़ा anticlimactic है
  • Oberon+ IDE से अभी कोशिश की, तो इस module से 2048-byte Hello.dll निकला
    module Hello
    begin
    println("hello world")
    end Hello
    https://github.com/rochus-keller/Pelib assembly generator का उपयोग करता है

    • Nicklaus Wirth सही थे
      Lazarus/FreePascal पहली बार इस्तेमाल करते समय मुझे ऐसा लगा था, और उनकी writings जितनी ज़्यादा पढ़ीं, उतना ही यह और सही लगा
      हाल में मुझे लगता है कि इस तरह की सोच के “punarjanm” से बाहर निकलने का रास्ता शायद WASM हो सकता है, लेकिन hardware को यूँ छोड़ना मेरे बस का नहीं
      ऐसी आध्यात्मिक मशीन को स्वीकार करने के लिए मैं बहुत primitive हूँ जिसे मैं अपनी desk पर हथौड़े से तोड़ नहीं सकता
      Ethereum जैसा अविनाशी global computer, जो किसी और के hardware पर हजारों डॉलर का compute time खर्च करते हुए चलता है और RPi से भी धीमा है, किसी बुनियादी स्तर पर मुझे taboo जैसा लगता है
      अब भी चाहता हूँ कि Wirth के चेहरे के साथ “Wirth was right” को T-shirt पर डालूँ
    • Oberon+ है तो शायद self-hosting होना चाहिए
      C++ ecosystem का कचरा बहुत ज़्यादा है
      GitHub और GitLab के alternatives के रूप में promote किए जाने वाले open source software hosting forge projects याद आते हैं। लेकिन जब code clone करने या bug file करने जाते हैं, तो सब GitHub पर ही मिलते हैं
    • उत्सुक हूँ कि file का end 0 bytes से भरा है या नहीं
      इसे disable करने का कोई option होना चाहिए
  • .exe को 1KB से कम तक घटाया जा सकता है—यह बात वाकई आंखें खोलने वाली थी
    अगर कोई Flutter के Hello World .exe को 1MB तक घटा सके, तो वह सच में कमाल होगा

    • पहला कदम शायद Microsoft को Windows में Flutter शामिल करने के लिए मनाना होगा
    • सोच रहा हूं कि क्या सिर्फ Dart में "Hello world" compile करना मान्य माना जाएगा