न्यूनतम .NET Hello World बाइनरी कितनी छोटी हो सकती है?
(blog.washi.dev)- .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#USstream हटाई गई, और 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
Programclass के अलावा छिपा हुआ<Module>type
- extreme reduction के लिए C# compiler settings और खोजने के बजाय, AsmResolver से .NET executable file सीधे बनाने का फैसला किया गया
- सीधे बनाए गए module में
<Module>type मेंmainfunction रखा गया, और CIL body में केवलLdstr "Hello World",Console.WriteLine,Retशामिल किए गए - इस तरीके से file size पहले की तुलना में लगभग आधा हो गया
64-bit PE से imports और base relocations हटाना
- बनाई गई executable file में
.textऔर.relocदो sections हैं, और Imports व Base Relocations data directories बड़ी जगह लेती हैं - AnyCPU या 32-bit .NET executable file में unmanaged entry point को
mscoree!_CorExeMaincall करना पड़ता है, इसलिए imports directory चाहिए - default .NET executable file relocatable होती है, इसलिए इस imported function call के लिए base relocation भी साथ में चाहिए
- base relocation default रूप से अलग section में जाती है, और प्रत्येक section कम से कम
0x200byte units में align होता है, जिससे file कम से कम 1KB से अधिक फूल जाती है - 64-bit .NET executable file में unmanaged entry point की जरूरत नहीं होती, इसलिए नीचे की settings से दोनों directories और
.relocsection हटाए जा सकते हैंmodule.PEKind = OptionalHeaderMagic.PE64module.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) तक घट चुकी थी
#Stringsheap .NET binary में second largest metadata stream है, और table stream#~द्वारा इस्तेमाल किए जाने वाले type और method names store करती है- runtime पर महत्वपूर्ण न होने वाले names को
nullset करने पर भी program सामान्य रूप से चलता है, और इससे 32 bytes घटे #GUIDstream Module table केMvidद्वारा refer किए गए GUID को रखती है, लेकिन प्रयोग में version identification की जरूरत न होने से इसे हटाया गयाMvidके 16 bytes हटाए गए- खाली
#GUIDstream पूरी और 16-byte header भी हटाया गया - कुल 32 bytes घटे
Console::WriteLinecall के लिए 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 के#USmetadata stream में store होता है, और length prefix वाले 16-bit character array तथा अतिरिक्त 0 byte के रूप में represent होता है- output की जाने वाली string के सभी characters के code point
0xFFसे छोटे हैं, इसलिए उन्हें 1-byte ASCII में represent किया जा सकता है, लेकिन CLR से#USstream की string को ASCII के रूप में interpret कराने का कोई तरीका नहीं है Console::WriteLineके बजाय unmanaged functionucrtbase!putsको P/Invoke से call किया गया- C# declaration form है
[DllImport("ucrtbase")] static extern int puts(nint str); putszero-terminated ASCII string pointer को argument के रूप में लेता है
- C# declaration form है
- compile time पर string का runtime address जानने के लिए PE optional header के
DllCharacteristicsमें DynamicBase flag बंद किया गया- fixed image base set किया गया
- ASCII string को
.textsection में रखा गया - runtime address
module_base_address + rva_ascii_stringसे calculate किया गया
- CLR इस flag का सम्मान करे, इसके लिए .NET data directory का ILOnly flag भी unset किया गया
- इस बदलाव से wide-character string,
#USstream,System.Console::WriteLinereference हटे, और उनकी जगहputsdefinition व 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 में नहीं, बल्कि
RAXregister में जाती है - P/Invoke definition को
voidreturn में बदलने सेmainmethod में return value हटाने के लिएpopinstruction खत्म किया जा सकता है - 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 नहीं किया जाता, इसलिएmscorlibreference भी हटाया गया- tables stream
#~की एक table कम हुई #Stringsstream सेmscorlibname भी हट गया
- tables stream
- .NET metadata directory का
VersionStringdefault रूप से"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 टिप्पणियां
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"
https://learn.microsoft.com/en-us/dotnet/core/extensions/cha...
याद पड़ता है कि download size का काफी बड़ा हिस्सा खुद .NET libraries थीं
यह भी सोचता हूं कि क्या browser को target करने वाला और C# इस्तेमाल करते हुए छोटे binaries बनाने वाला कोई 2D game framework/engine है
यह लेख भ्रम पैदा कर सकता है। इस 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 भी शामिल करने पड़ते हैं, इसलिए इसका काफी बड़ा होना स्वाभाविक है
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...
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 में जोड़ना होगा
इसे ऐसे call करें:
echo "hello world"खास तौर पर .NET Framework 4.7.2 supported Windows versions में default रूप से शामिल होगा
modern Windows install में .NET न हो, यह संभव नहीं लगता, और Linux तथा macOS में यह default रूप से नहीं होता; इसे fairness का आधार बनाना अजीब लगता है
वैसे, मुझे उत्सुकता है कि क्या किसी ने देखा कि लेखक की लोकेशन Antarctica McMurdo Station है
मुझे अंदाज़ा नहीं था कि उस स्टेशन पर इस तरह के tech stack वाला कोई होगा: https://usscar.org/directory?combine=&field_usscar_nsf_progr...
यह काफ़ी दिलचस्प कहानी बन सकती है
बाहरी लोगों को जो बात चौंकाती है, वह यह है कि वहाँ ज़्यादातर लोग support staff हैं, और वैज्ञानिक एक छोटा हिस्सा हैं, शायद लगभग 20% ही
support staff में firefighters, carpenters, cooks, mechanics आदि—एक आधुनिक शहर चलाने के लिए ज़रूरी हर तरह के पेशे—शामिल होते हैं
शायद selection process की वजह से, लगभग सभी बेहद काबिल होते हैं और अपने क्षेत्र में शीर्ष स्तर के होते हैं। व्यापक skills वाले Swiss Army knife जैसे लोग आश्चर्यजनक रूप से बहुत थे
वहाँ रहने और काम करने की कठिनाइयाँ अनोखे और बहुत motivated लोगों को रोशनी की ओर खिंचे कीड़ों की तरह आकर्षित करती हैं, और कई लोग साल-दर-साल लौटते हैं। शानदार community है
वहाँ और क्या कर सकते हैं?
.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 उस पर आधारित नहीं बनाने चाहिए
उम्मीद थी कि बात “वास्तविक” binary की होगी, यानी .NET AOT जैसे natively compiled .NET executable की
https://learn.microsoft.com/en-us/dotnet/core/deploying/nati...
सबसे ज़्यादा 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.jpgOberon+ IDE से अभी कोशिश की, तो इस module से 2048-byte Hello.dll निकला
module Hellobeginprintln("hello world")end Hellohttps://github.com/rochus-keller/Pelib assembly generator का उपयोग करता है
Lazarus/FreePascal पहली बार इस्तेमाल करते समय मुझे ऐसा लगा था, और उनकी writings जितनी ज़्यादा पढ़ीं, उतना ही यह और सही लगा
हाल में मुझे लगता है कि इस तरह की सोच के “punarjanm” से बाहर निकलने का रास्ता शायद WASM हो सकता है, लेकिन hardware को यूँ छोड़ना मेरे बस का नहीं
ऐसी आध्यात्मिक मशीन को स्वीकार करने के लिए मैं बहुत primitive हूँ जिसे मैं अपनी desk पर हथौड़े से तोड़ नहीं सकता
Ethereum जैसा अविनाशी global computer, जो किसी और के hardware पर हजारों डॉलर का compute time खर्च करते हुए चलता है और RPi से भी धीमा है, किसी बुनियादी स्तर पर मुझे taboo जैसा लगता है
अब भी चाहता हूँ कि Wirth के चेहरे के साथ “Wirth was right” को T-shirt पर डालूँ
C++ ecosystem का कचरा बहुत ज़्यादा है
GitHub और GitLab के alternatives के रूप में promote किए जाने वाले open source software hosting forge projects याद आते हैं। लेकिन जब code clone करने या bug file करने जाते हैं, तो सब GitHub पर ही मिलते हैं
इसे disable करने का कोई option होना चाहिए
.exe को 1KB से कम तक घटाया जा सकता है—यह बात वाकई आंखें खोलने वाली थी
अगर कोई Flutter के Hello World .exe को 1MB तक घटा सके, तो वह सच में कमाल होगा