- PDP-11 के दौर में C hardware abstraction से अच्छी तरह मेल खाती थी, लेकिन आधुनिक CPU में C की abstract machine — sequential execution और flat memory — वास्तविक hardware से काफी अलग पड़ती है
- Spectre और Meltdown का संबंध इस बात से है कि processor C के sequential model को तेज़ी से चलाने के लिए branch prediction, speculative execution और instruction-level parallelism पर बहुत निर्भर हो गए
- C code को तेज़ बनाने के लिए सिर्फ machine code में सरल रूपांतरण काफी नहीं; LLVM/Clang स्तर की जटिल optimizations चाहिए, और कुछ optimizations C semantics से टकरा सकती हैं
- pointer provenance, structure padding, uninitialized values, signed integer overflow जैसे नियम execution result का अनुमान कठिन बनाते हैं और security vulnerabilities तक ले जा सकते हैं
- आधुनिक hardware से बेहतर मेल खाने वाला model बहुत सारे threads, चौड़ी vector units और सरल memory model का उपयोग करता है, लेकिन मौजूदा C code compatibility सबसे बड़ी बाधा बनी रहती है
C के “low-level” जैसा दिखने की वजह
- low-level भाषा में hardware द्वारा दी गई abstraction और भाषा की abstract machine के बीच mapping आसान होनी चाहिए
- PDP-11 पर C को low-level भाषा माना जा सकता था
- program sequentially execute होता था
- memory को flat space की तरह扱ा जाता था
- pre-increment और post-increment operators PDP-11 addressing modes से अच्छी तरह मेल खाते थे
- Alan Perlis ने कहा था, “जब किसी program को अप्रासंगिक चीज़ों पर ध्यान देना पड़े, तो वह भाषा low-level है,” लेकिन केवल यह परिभाषा low-level भाषा से लोगों की “hardware के करीब” होने की अपेक्षा को पर्याप्त रूप से नहीं समझाती
आधुनिक CPU तेज़ PDP-11 emulator की तरह काम करते हैं
- Spectre और Meltdown का मूल कारण सिर्फ तेज़ processor बनाना नहीं है; यह processor design से भी जुड़ा है जिसने PDP-11 जैसी abstract machine को तेज़ी से expose करने की कोशिश की
- C code ने C11 से पहले, non-standard vendor extensions को छोड़ दें तो, व्यवहार में पूरी तरह sequential machine उपलब्ध कराई; C11 के बाद भी अधिकतर sequential abstract machine बनी रही
- आधुनिक CPU execution units को लगातार busy रखने के लिए instruction-level parallelism (ILP) निकालते हैं
- पास-पास के operations की जांच करते हैं और independent operations को parallel में issue करते हैं
- programmer को अधिकतर sequential code लिखने देने की कीमत पर complexity और power consumption बढ़ती है
- GPU ऐसी logic के बिना भी high performance दे सकते हैं, लेकिन वे explicit parallel programs मांगते हैं
Spectre, Meltdown और speculative execution की लागत
- आधुनिक Intel processor एक समय में अधिकतम 180 instructions को in-flight रख सकते हैं
- C code में औसतन लगभग हर 7 instructions पर एक branch माना जा सकता है
- single thread में pipeline भरने के लिए अगले 25 branches के targets का अनुमान लगाना पड़ता है
- गलत अनुमान काम कर लेने के बाद उसे फेंक देने जैसा result बनाता है और power भी बर्बाद करता है
- Spectre और Meltdown ने इसी discarded work के visible side effects को side channel के रूप में exploit किया
- आधुनिक high-performance core का register rename engine die area और power के बड़े उपभोक्ताओं में से एक है
- instruction execute हो रहा हो तो उसे बंद करना या power-gate करना मुश्किल होता है
- GPU में ऐसी units नहीं होतीं; parallelism कई threads से आता है
C का flat memory model cache की वास्तविकता से मेल नहीं खाता
- C abstract machine का core, flat memory, 20 साल से भी अधिक समय से वास्तविक hardware से मेल नहीं खाता
- आधुनिक processors में registers और main memory के बीच आमतौर पर 3-level cache होता है
- cache, नाम के मुताबिक, programmer से छिपा रहता है और C में दिखाई नहीं देता
- आधुनिक processor पर तेज़ code बनाने के लिए cache का efficient इस्तेमाल जरूरी है
- C programmer को performance पाने के लिए सिर्फ abstract machine नहीं, implementation details भी जाननी पड़ती हैं
- उदाहरण के लिए 64-byte aligned दो values एक ही cache line में आ सकती हैं
C code को तेज़ बनाने के लिए जरूरी compiler complexity
- low-level भाषा हो तो complex compiler के बिना भी उसे तेज़ code में आसानी से बदला जाना चाहिए, लेकिन C के साथ ऐसा नहीं है
- Clang और उससे जुड़े LLVM हिस्से लगभग 20 लाख lines के हैं
- C को तेज़ चलाने के लिए जरूरी analysis और transformation passes ही गिनें तो comments और blank lines हटाकर लगभग 2 लाख lines तक पहुंचते हैं
- C में bulk data process करते समय आमतौर पर हर element को sequentially process करने वाला loop लिखा जाता है
- आधुनिक CPU पर optimal execution के लिए compiler को पहले loop iterations के बीच independence तय करनी पड़ती है
restrictkeyword यह guarantee दे सकता है कि एक pointer के जरिए write, दूसरे pointer के जरिए read में interfere नहीं करेगा
- ऐसी जानकारी देने के मामले में Fortran, C से बेहतर है, और यही high-performance computing में C के Fortran की जगह न ले पाने की प्रमुख वजहों में से एक है
vectorization और C memory layout guarantees का टकराव
- अगर loop iterations independent हों, तो compiler result को vectorize करने की कोशिश करता है
- आधुनिक processors scalar code की तुलना में vector code में 4–8 गुना throughput पा सकते हैं
- ऐसे processors के लिए low-level भाषा में arbitrary length के native vector types होना स्वाभाविक होगा
- LLVM IR ऐसा model देता है, क्योंकि बड़े vector operations को छोटे operations में बांटना उल्टा करने से आसान है
- C की memory layout guarantees optimizations से टकराती हैं
- समान prefix वाले structures को interchangeable तरीके से इस्तेमाल किया जा सकता है
- structure field offsets भाषा में expose होते हैं
- compiler के लिए vectorization सुधारने हेतु field order बदलना या padding insert करना मुश्किल होता है
- data structure layout पर fine-grained control low-level भाषा का फायदा हो सकता है, लेकिन यही C को तेज़ बनाना भी कठिन करता है
padding, SROA और loop unswitching की समस्या
- C arrays के अंदर padding न होने की guarantee देने के लिए structure के अंत में padding मांगता है
- structures में
memcmpजैसी type-agnostic comparison संभव होनी चाहिए, इसलिए structure copy को padding भी बनाए रखनी पड़ती है- कुछ experiments में कुछ workloads के कुल execution time का उल्लेखनीय हिस्सा padding copy करने में गया
- SROA एक optimization है जो structures और fixed-length arrays को individual variables से replace करने की कोशिश करती है
- यह accesses को independently handle करने देता है और ऐसे operations हटाने देता है जिनका result observe नहीं होता
- कुछ cases में यह padding हटा देता है, लेकिन हमेशा नहीं
- loop unswitching वह optimization है जिसमें condition वाले loop से condition को बाहर निकालकर दोनों paths में loop रख दिया जाता है
- यह इस विचार से टकराता है कि low-level भाषा का code execute होते समय programmer जानता है कि कौन-सा code कब execute होगा
- C के unspecified value और undefined behavior concepts के साथ भी यह समस्याएं पैदा कर सकता है
uninitialized values और undefined behavior
- C में uninitialized variable पढ़ने पर वह unspecified value बन जाता है, और हर read पर उसका value अलग हो सकता है
- यह rule pages के delayed recycling जैसे behavior की अनुमति देता है
- FreeBSD का
mallocimplementation अभी इस्तेमाल न हो रहे pages के बारे में operating system को बताता है - operating system किसी page पर first write को इस hint के रूप में इस्तेमाल कर सकता है कि वह page फिर से चाहिए
- FreeBSD का
- यदि unspecified value flow control में इस्तेमाल हो, तो यह undefined behavior बन जाता है
- उदाहरण के लिए
ifcondition में uninitialized value इस्तेमाल करना
- उदाहरण के लिए
- loop unswitching में अगर loop 0 बार execute हो, तो मूल code में पूरा loop body dead code होता है
- unswitching के बाद संभवतः uninitialized variable से branch किया जा सकता है
- यानी dead code undefined behavior में बदल जाता है
- C code को तेज़ बनाया जा सकता है, लेकिन पर्याप्त smart compiler बनाने में हजारों person-years लगते हैं और कभी-कभी भाषा के कुछ rules तोड़ने पड़ते हैं
C को समझना कठिन क्यों हो गया
- low-level भाषा हो तो programmer abstract machine और वास्तविक physical machine की mapping आसानी से समझ सके
- PDP-11 पर C expressions एक-दो instructions में आसानी से map हो जाते थे, और local variables व primitive types भी hardware से सरलता से correspond करते थे
- बाद में C implementations तेज़ code और hardware mapping का भ्रम बनाए रखने के लिए लगातार जटिल होती गईं
- 2015 में C programmers, compiler writers और standards committee members पर किए गए survey में C की comprehensibility problem सामने आई
- किसी structure को 0 से initialize करने के बाद कुछ fields set करने पर padding bits सभी 0 होंगे या नहीं, इस पर 36% ने हां में भरोसा जताया और 29% ने कहा कि वे नहीं जानते
- वास्तविक result compiler और optimization level के अनुसार बदल सकता है
pointer provenance और security vulnerabilities
- BCPL model अपेक्षाकृत सरल था: value एक word है, और हर word data या data का address है
- C model को segment architectures या garbage-collected virtual machines सहित विभिन्न targets पर implement करने के लिए design किया गया था
- C standard ऐसे systems में problems से बचने के लिए pointer पर valid operations सीमित करता है
- C Defect Report 260 ने pointer definition में pointer provenance concept शामिल किया
- implementation bit pattern के origin को track कर सकता है
- bit-level पर समान होने पर भी अलग origins के pointers को distinguish कर सकता है
provenanceशब्द C11 specification में नहीं आता, इसलिए compiler writers को उसका अर्थ तय करना पड़ता है- GCC और Clang में यह फर्क है कि pointer को integer में बदलकर फिर pointer में बदलने पर provenance बना रहता है या नहीं
- signed integer overflow और null check से पहले pointer dereference करने वाले code में security vulnerabilities देखे गए cases हैं
- null pointer dereference C में undefined behavior है, इसलिए compiler मान सकता है कि जो pointer पहले ही dereference हो चुका है वह null नहीं हो सकता
- उदाहरण के तौर पर CVE-2009-1897 है
C नहीं, किसी और तरह के processor की कल्पना करना
- Spectre और Meltdown के लिए सुझाए गए fixes काफी performance penalty लगाते हैं और पिछले 10 सालों की microarchitecture progress के बड़े हिस्से को neutralize करते हैं
- C code को तेज़ बनाने के बजाय अब तेज़ processor के अनुरूप programming model पर फिर से सोचने का समय है
- Sun/Oracle UltraSPARC Tx जैसे highly multithreaded chips को execution units भरने के लिए cache की बहुत जरूरत नहीं होती
- पर्याप्त high-level parallelism हो तो memory का इंतजार कर रहे thread को रोककर दूसरे thread के instructions से execution units भरी जा सकती हैं
- समस्या यह है कि C programs में busy threads कम होने की प्रवृत्ति होती है
- ARM SVE (Scalar Vector Extensions) program और hardware के बीच बेहतर interface का एक उदाहरण है
- मौजूदा vector units fixed-size vector operations expose करती हैं और उम्मीद करती हैं कि compiler algorithm को उस size के अनुसार fit करे
- SVE में programmer available parallelism की degree describe करता है, और hardware उसे execution units की संख्या के हिसाब से map करता है
- C में autovectorizer को loop structure से parallelism infer करना पड़ता है, इसलिए यह complex है; लेकिन functional style के
mapoperation में target array की length ही available parallelism होती है, इसलिए code generation सरल होता है
सरल memory model और parallel programming
- आधुनिक CPU में cache coherency protocol तेज़ और सही बनाना सबसे कठिन हिस्सों में से एक है
- complexity का बड़ा हिस्सा उन भाषाओं को support करने से आता है जो data को shared और mutable मानती हैं
- Erlang-style abstract machine में सभी objects या तो thread-local होते हैं या immutable
- Erlang में प्रति thread केवल एक mutable object वाला सरल model होता है
- ऐसे system का cache coherency protocol mutable या shared, इन दो cases में बांटा जा सकता है
- immutable objects cache को सरल बना सकते हैं और कई operations को सस्ता बना सकते हैं
- Sun Labs के Project Maxwell ने यह देखा कि cache में मौजूद objects और young generation में allocate होने वाले objects लगभग वही set होते हैं
- यदि object cache से evict होने से पहले मर जाए, तो उसे main memory में वापस न लिखकर power बचाई जा सकती है
- heap में immutable objects और mutable stack इस्तेमाल करने पर garbage collector ऐसा सरल state machine बन सकता है जिसे hardware में implement करना आसान हो
- केवल speed के लिए design किया गया processor कई threads, wide vector units और सरल memory model support करने की संभावना रखता है
- ऐसे system पर C code चलाना problem हो सकता है
- दुनिया भर में बहुत सारा legacy C code होने के कारण commercial success मुश्किल है
- parallel programming कठिन है — यह आम धारणा C जैसी abstract machine वाली भाषाओं में parallel programming पर अधिक सटीक लागू होती है
- Alan Kay ने actor-model languages बच्चों को सिखाईं, और उन्होंने 200 से अधिक threads वाले working programs लिखे
- Erlang programmers अक्सर हजारों parallel components वाले programs लिखते हैं
- multicore CPU और many-core GPU के व्यापक उपयोग की स्थिति में C आधुनिक hardware पर अच्छी तरह map नहीं होती
1 टिप्पणियां
Hacker News की रायें
C के low-level होने की वजह कम-से-कम manual memory management तो है ही
खासकर आधुनिक hardware में memory management programming के केंद्र में है। Rust बिना garbage collector के memory safety को आगे रखता है, यह भी आखिर इसलिए है कि Rust के अस्तित्व की मुख्य वजह memory management के काफ़ी करीब है। C तेज़ है तो वजह memory है, और C unsafe है तो ज़्यादातर वजह भी memory ही है। parallel computing कठिन होने की बड़ी वजहों में से एक concurrent memory access भी है। Functional programming अक्सर mathematical concepts से घिरी होती है, लेकिन उसका बड़ा हिस्सा objects को immutable जैसा दिखाने और अंदर compiler द्वारा mutable memory संभालने में है
C में अगर allocator इस्तेमाल करें, तो उसकी हर call explicit होती है। पुराने C++ में
new/deleteऔर raw pointers allocator को explicit रूप से बुलाते भी हैं, लेकिन बहुत कुछ destructor में अपने-आप भी होता है। आधुनिक C++ के smart pointers allocation और deallocation दोनों को automatic बना देते हैं, इस मायने में वे मूलतः garbage collection languages जैसे ही हैंआप processor को यह निर्देश नहीं दे सकते कि कौन-सा data cache के किस level में रखना है, क्या virtual memory में भेजना है, वगैरह। यह Python से तो low-level है, लेकिन PDP-11 के दौर के C जैसी low-level memory management मानना मुश्किल है
microcontroller-level systems या MMU-रहित systems में कहानी अलग होती है, लेकिन वह फिर अलग मुद्दा है
Rust developer होने के बावजूद मैं भी इस भ्रम में काम करता हूँ कि pointers memory addresses जैसे वास्तविक physical objects हैं। Rust और कुछ हद तक C++ references और borrowing जैसी management abstractions को सामने रखते हैं, लेकिन core concept वही रहता है
असल में operating system kernel physical memory और program के बीच एक विशाल layer रखता है, और “addresses” व “pointers” OS और MMU द्वारा तरह-तरह की processing किए जाने वाले handles के ज़्यादा करीब हैं
“raw pointer” भी दरअसल raw नहीं होता। वह page के भीतर offset का handle है, और actual pages इधर-उधर बिखरे हो सकते हैं। libc और C model से पूरी तरह बाहर निकलकर VM subsystem के pages से सीधे interact करने वाली pure references, एक तरह की “object handle” दुनिया में जाएँ, तो शायद वास्तविक lower subsystem behavior के और करीब पहुँचा जा सकता है
mallocऔरfreelibrary functions हैंhardware में उस तरह की byte-level allocation नहीं होती, इसलिए यह hardware के ऊपर abstraction ही नहीं, बल्कि operating system memory allocate कैसे करता है, उसका भी abstraction है
C में stack तक भी direct access नहीं मिलता। stack frame abstracted होता है, और इस्तेमाल करने को बस
longjmpजैसा कुछ हैundefined behavior और strict aliasing rules तक का ध्यान रखें, तो memory को मनमाने ढंग से कुरेदने की access भी बहुत ज़्यादा नहीं बचती
C प्रोग्रामर और compiler लेखक के रूप में, जो लोग C को समझते हैं और professional तौर पर इस्तेमाल करते हैं, उनके लिए C निश्चित रूप से low-level language है
अगर आप low-level language खोज रहे हैं, तो C और उसके रिश्तेदार सबसे अच्छे विकल्प हैं
अगर आप अभी C सीखना शुरू कर रहे हैं और जानना चाहते हैं कि इसे experts की तरह कैसे लिखा जाए, तो इस लेख को अनदेखा करना बेहतर है। यह सिर्फ भ्रम पैदा कर सकता है और C को प्रभावी ढंग से इस्तेमाल करने की आपकी क्षमता घटा सकता है
यह ऐसी machine तक low-level access देता है जिसे असली machine को काफी मेहनत से emulate करना पड़ता है। असली machine तक पहुंचने के लिए वर्षों में जो ढीले-ढाले उपकरण और patch जोड़े गए, वे C के भीतर अपेक्षाकृत पराये तत्व हैं
हालांकि मैं मानता हूं कि शीर्षक rhetoric के लिहाज से कठोर है। गलत low-level language होने से कोई भाषा high-level नहीं बन जाती। WASM भी अगर आधुनिक hardware से सीधे मेल खाने का दावा करे तो “गलत” होगा, लेकिन इसका मतलब यह नहीं कि वह high-level है
C का खराब mapping होना अपने-आप में निराशाजनक नहीं है। यह 1970 के दशक की भाषा है, इसलिए ऐसा हो सकता है, और आज भी कई मामलों में यह निश्चित रूप से उपयोगी है। ज्यादा निराशाजनक बात यह है कि C अब भी language design को काफी हद तक प्रभावित करता है, और language designers के hardware को देखने के तरीके को गहराई से रंग देता है। इसलिए आधुनिक language design अक्सर hardware से अच्छी तरह मेल खाने वाली भाषा बनाने के बजाय C के टुकड़ों को फिर से मिलाने तक सीमित रह जाता है
अगर आप सोचते हैं कि आपके लिखे code का assembly से one-to-one संबंध होगा, तो समस्या आएगी। ऐसी चीजें कैसे अड़ंगा लगाती हैं, इसे गहराई से देखना हो तो https://youtu.be/w3_e9vZj7D8 देखें
लेखक का मुख्य मुद्दा शायद यह नहीं है कि “C system programming के लिए अच्छी language नहीं है।” Haskell में
volatile int *dma_register = SCATTER_GATHER_BASE;जैसी चीज़ बराबरी से लिखना मुश्किल हैलेखक का आशय यह है कि C और अन्य “von Neumann machine को model करने वाली” languages को तेज चलाने की प्रेरणा ने compilers को बहुत जटिल बना दिया है, और लेखक यह संकेत देता है कि “low-level होने पर simple compiler चाहिए।” ऐसे code को तेज चलाने के लिए बनाए गए processors भी बहुत जटिल हैं, और उस जटिलता की कीमत होती है
कई मायनों में यह programming model shift की मांग करने वाला लेख है, और GPU को इस बात के उदाहरण के रूप में देता है कि जब “नया programming model” और “उसे support करने वाला silicon” साथ बनते हैं तो क्या संभावनाएं होती हैं
मूल अर्थ लेख में इस्तेमाल किए गए अर्थ के ज्यादा करीब है। low-level language portable नहीं होती और जिस hardware पर चलती है उससे बंधी होती है, जबकि high-level language कई platforms को target कर सकती है। इस परिभाषा में C साफ तौर पर high-level language है
शिकायत यह नहीं कि लेखक wordplay कर रहा है, बल्कि यह है कि पुराने शब्द से चिपके रहना उल्टा समझ को धुंधला करता है। “generation” classification आम तौर पर ज्यादा explanatory है
पहली generation machine code है, दूसरी generation assembly, तीसरी generation general-purpose languages, और चौथी generation application-domain-specific languages हैं
तीसरी और चौथी generation का फर्क कभी-कभी धुंधला हो जाता है, और 80–90 के दशक में 5th generation की भी बातें थीं जो आखिरकार जम नहीं सकीं। फिर भी SQL, HyperCard, Mathematica को मैं काफी स्पष्ट 4th-generation language examples मानता हूं
यह तरीका अच्छा इसलिए है क्योंकि यह languages को इस आधार पर बांटता है कि उन्हें कब इस्तेमाल किया जाता है, जिसमें फर्क अपेक्षाकृत साफ है। उसके बाद “high-level/low-level” को relative term की तरह इस्तेमाल किया जा सकता है। जितनी ज्यादा high-level language होती है, वह computer वास्तव में क्या करता है, उसकी details को उतना ही ज्यादा abstract करने की प्रवृत्ति रखती है। ऐसा करने पर भी higher-generation languages के आम तौर पर ज्यादा high-level होने की बात बनी रहती है, और जो खोता है वह सिर्फ पूरी तरह arbitrary और सच कहें तो बेकार boundary line को लेकर होने वाली मूर्खतापूर्ण बहस है
इस तरीके से .NET IL, WebAssembly, Java bytecode को बहुत high-level 2nd-generation languages माना जा सकता है, जो दिलचस्प है। और Forth 3rd-generation language है। Chuck हो तो आकर भिड़ सकता है
यह hammer को कैसे इस्तेमाल करें, इसकी बात नहीं, बल्कि hammer को हर जगह इस्तेमाल करने के तरीके—यानी C design—से हम सीमित हो रहे हैं या नहीं, यह पूछने जैसा है
लेखक के इस दावे से मैं सहमत नहीं हूं कि CPU instruction set को CPU implementation को और ज्यादा expose करना चाहिए
अतीत में भी कोशिशें हुई हैं और लंबे समय में वे असफल रहीं। उदाहरण के लिए 80 के दशक के अंत और 90 के दशक की शुरुआत में design किए गए कुछ RISC processors, जैसे MIPS और SuperH के branch delay slots। जिन्हें concept नहीं पता, उनके लिए: branch instruction के बाद वाली instruction इस बात से स्वतंत्र होकर execute होती है कि branch ली गई या नहीं
short term में इससे branch के बाद pipeline stall से बचने का काम programmer पर डालकर processor को ज्यादा simple और सस्ता बनाया जा सकता था। लेकिन समय के साथ processor design और pipelines ज्यादा जटिल हो गए, और केवल एक instruction branch delay को ढकने के लिए पर्याप्त नहीं रही। आखिरकार compatibility के कारण यह एक legacy बन गया जिसे future processors को संभालना पड़ा, और इसने branch prediction और pipeline logic को और जटिल बना दिया
गलत details expose करना जाहिर तौर पर खराब है। वह बस यह कह रहा है कि modern CPU दुनिया में C model की गंभीर सीमाएं हैं
मैंने किसी processor के ऐसे subsystem को developers द्वारा इस्तेमाल किए जाने पर एक presentation सुनी थी। अगर उसे न इस्तेमाल करें तो time window का 95% सिर्फ data copy करने में जाता है, लेकिन उस engine से data पहले से request करने पर data acquisition में time window का सिर्फ 10% लगता है, और पूरी time window के लगभग 50% के भीतर वे अपना काम खत्म कर लेते हैं, जिससे extra features और improvements के लिए काफी समय बचता है
अगर x86 में ऐसी capability होती, तो मैं PhD के दौरान access किए जाने वाले matrix data को पहले से request करने में इसका इस्तेमाल करता। मेरा access pattern linear नहीं है, लेकिन well-defined है। अभी उस code को और accelerate करना हो तो prefetcher को पसंद आए, इसलिए matrices को rearrange करना होगा और पूरे codebase को ऊपर से नीचे तक refactor करना होगा
चाहें तो खराब design किया जा सकता है, लेकिन generalize करने लायक ऐतिहासिक उदाहरण कितने हैं, यह जानना चाहूंगा
लो-लेवल से हाई-लेवल को द्विभाजन नहीं, बल्कि स्पेक्ट्रम मानता हूँ
C को भाषाओं में निचले एक-तिहाई हिस्से में कहा जा सकता है, और यह memory तथा thread management जैसे कई machine primitives के सामने expose करता है। Assembly जितना low नहीं है, फिर भी Java या Go से low है, और Python या JavaScript की तरफ़ से तो साफ़ तौर पर काफ़ी दूर है
इसके अलावा C segmented memory या non-flat addresses इस्तेमाल करने वाले platforms के लिए काफ़ी अनुपयुक्त है। ऐसी चीज़ों के फिर से चलन में आने के संकेत दिख रहे हैं, और C का व्यापक प्रसार इसमें सचमुच बहुत बड़ी बाधा है
इसलिए मेरे दिमाग़ का model हमेशा यही रहा है कि “C वह सबसे निचला स्तर है जहाँ तक processor को सीधे निर्देश देने से पहले उतरा जा सकता है”
“C किसी सामान्य ‘high-level’ भाषा की तरह व्यवहार नहीं करती। क्योंकि यह ऐसी कई सुविधाएँ देती है जिन्हें आम तौर पर assembly language जैसी ‘low-level’ भाषाओं से जोड़ा जाता है। इनमें किसी खास memory address पर data लिखने और पढ़ने की क्षमता, memory locations की सामग्री पर operations करने की क्षमता, integer variables को increment/decrement करने वाले instructions आदि शामिल हैं … इसलिए C programmer को low level पर काम करने की flexibility और efficiency देती है, साथ ही आज की computer languages में सामान्य अधिक उन्नत data structures और program flow control जैसे high-level कामों के फायदे भी देती है। इसी वजह से C को कभी-कभी ‘high-level low-level language’ या ‘low-level high-level language’ कहा जाता है।” - https://archive.org/details/computerprogramm0000ford/page/13...
लेख के अंत में “software development में parallel programming कठिन है—यह एक आम मिथक है” वाला वाक्य भ्रामक है
लेखक ऐसी खास परिस्थितियाँ बताता है जहाँ यह कठिन नहीं है, लेकिन अगर सवाल सामान्य रूप से लागू हो, तो parallel programming कठिन है, और यह कोई आम मिथक नहीं है
क्या parallel programming कठिन है? अगर और विस्तृत conditions के बिना पूछा जाए, तो हाँ। code instructions के क्रम से एक-एक करके execute होने की तुलना में उनके एक साथ execute होने की कल्पना करना कहीं ज़्यादा कठिन है
(map inc [0 1 2 3])program करते समय, हर element परincfunction के sequentially चलने और parallel में चलने की कल्पना करने की कठिनाई में वाकई फर्क है?मुझे लगता है parallel programming की कठिनाई जन्मजात नहीं, बल्कि दो चीज़ों जैसी है
पहला, languages आम तौर पर sequential execution को default मानती हैं, इसलिए async करने के लिए programmer को extra primitives लाने पड़ते हैं
दूसरा, यह जानना पड़ता है कि parallel programming को कब प्रभावी ढंग से इस्तेमाल करना है
अगर आपके पास independent elements की list या stream है जिन्हें सिर्फ independent computation चाहिए, तो parallel programming सहज है
लोग जहाँ अटकते हैं, वह तब है जब async को ऐसी जगह जबरन डालते हैं जहाँ उसकी जरूरत नहीं—यानी जहाँ performance sequential execution जैसी ही या उससे खराब हो—या जब असल में computations परस्पर निर्भर हों और async डालकर behavior तोड़ दें
जब आप “extra details या specificity न हो तो” कहते हैं, तो असल में आप C/C-family worldview को default framework की तरह इस्तेमाल कर रहे होते हैं
लेखक का point यह है कि sequential programming सिर्फ simple programming का एक प्रकार है, अकेला प्रकार नहीं, और modern hardware में आसानी से fit नहीं बैठता
Erlang मौजूद है और लोग उसे successfully इस्तेमाल करते हैं—इस तथ्य का मतलब यह नहीं कि जो चीज़ ज़्यादा कठिन है वह कठिन नहीं है
processes या threads जैसी concurrency programming infrastructure से parallel algorithms implement करना भी कठिन है। लेकिन parallel programming, यानी बहुत सारे processing elements से एक ही काम साथ में करवाना, सही abstraction हो तो कहीं ज़्यादा आसान है
हालांकि matrix multiplication जैसे कुछ use cases में exceptions हैं
यह लेख इस बात पर सही है कि computer कोई तेज़ PDP-11 नहीं है, लेकिन यह बात C से जुड़ी है—इस पर गलत है
उदाहरण के लिए इसमें वाक्य है: “C abstract machine memory model का एक और core हिस्सा है flat memory. यह 20 साल से भी ज़्यादा समय से सच नहीं रहा”
इसका C से कोई संबंध नहीं है। hardware इस abstraction को enforce करता है। और यह अच्छी बात है। वरना अलग cache वाली machine पर ले जाते ही program रुक जाएगा
जैसे flat RAM होने का दिखावा करने वाली hierarchical memory structure, instruction set से संकेत मिलने की तुलना में कहीं बड़ा CPU और out-of-order/speculative execution, तथा लिखे गए program और actual execution को और अलग करने वाले optimizing compilers
IBM, C के उभरने से बहुत पहले 1970s में ही इन चीज़ों पर काम कर रहा था। इस model की आलोचना करना और alternatives खोजना उचित है, लेकिन C को दोष देना fair नहीं है
यह लेख अब 5 साल पुराना है, और यह आधार कि कंप्यूटर संरचनात्मक रूप से PDP-11 जैसे बहुत नहीं हैं, अब तो और भी सही हो गया है, लेकिन “C-नुमा processor न सोचें” वाला निष्कर्ष अब कम मजबूत लगता है
हम linear code और बेहद parallel code के बीच एक मजबूत विभाजन देख रहे हैं, और 2018 में भी ऐसा था। इसका सबसे साफ उदाहरण machine learning और scientific computing में Python का उभरना है। जब performance सर्वोच्च प्राथमिकता नहीं होती, तब single-threaded style और flat memory model में लिखना अब भी बहुत सुविधाजनक है
जब performance महत्वपूर्ण हो जाती है, तो parallel programming के लिए ज्यादा उपयुक्त भाषा पर जाना ठीक है। Pytorch जैसी चीजों की computation graph language, CUDA के ऊपर primitive elements का अलग set, या Futhark जैसी अधिक experimental languages इसी में आती हैं। Performance-critical code हमेशा domain-specific languages लेकर आया है, और वे घटने के बजाय और आम होती लग रही हैं। Hardware भी इसी हिसाब से बनाया जा रहा है। Desktop PC में आम CPU+GPU संयोजन, x86 vector extensions जिनके primitive elements व्यवहार में अपना DSL बनाते हैं, और M1 जैसी चीजें जहाँ GPU को CPU से जोड़कर दोनों को उसी system memory तक तेज access दिया जाता है—ये उदाहरण हैं
दूसरे शब्दों में, सच में पुरानी चीज C नहीं, बल्कि शायद general-purpose language की वह धारणा है जो हर तरह के काम के लिए समान रूप से अच्छी हो
अगर modern CPU की जटिलता के कारण C अब “low-level” language नहीं रह गई, तो यही तर्क assembly language पर भी लागू होता है
क्योंकि out-of-order execution और register renaming जैसी चीजें assembly पर भी लागू होती हैं
हाल के दशकों में compilers के sophisticated होने से भी इस दलील को बल मिलता है। C compiler द्वारा generate की गई assembly, यानी object code भी loop hoisting, common subexpression elimination आदि के कारण उम्मीद से अलग निकल सकता है
फिर भी C को “low-level” language कहना अब भी एक उपयोगी label लगता है। नहीं तो इस नाम को ही retire करना पड़ेगा
यह सच है कि वह असली computer के ऊपर abstraction है, लेकिन C ने virtual computer model के ऊपर जो बनाया है उससे बहुत कम। आज की assembly, C के बनाए जाने के समय के C जैसी ही level पर है। आज का C इतना high-level है कि वह ऐसी capabilities नहीं देता जिन्हें बेहतर और modern languages से पाया न जा सके
हालांकि मैं सहमत हूँ कि आजकल “low-level” और “high-level” नाम बहुत उपयोगी नहीं हैं
लेख दो ऐसी argument lines चलाता लगता है जिन्हें आपस में मिलाना मुश्किल है
पहली यह दलील है कि C low-level language नहीं है, और उदाहरण के तौर पर struct padding और signed integer overflow का undefined behavior होना दिया गया है। यह हिस्सा समझ आता है, और किसी काल्पनिक “वास्तव में low-level” language के लिए language features सुझाने जैसा होने से constructive लगता है
दूसरी यह दलील है कि C के प्रभुत्व के कारण CPU designers को C को naturally execute करने वाली कोई चीज बनाने के लिए बहुत जोर लगाना पड़ा। इसमें register renaming, flat memory, caching जैसे उदाहरण हैं। यह दलील भी समझ आती है, लेकिन पहली दलील और लेख के शीर्षक के संदर्भ में यह कैसे जुड़ती है, यह मुझे ठीक से समझ नहीं आता। अगर इसे शाब्दिक रूप से लें तो modern hardware पर low-level language बनाना ही असंभव है, और machine code तक “high-level” है। तब निष्कर्ष यह होगा कि पहले hardware की नई पीढ़ी बनानी होगी जो instruction set architecture में कहीं अधिक complexity expose करे, और तभी उसका उपयोग करने वाली low-level language design की जा सकेगी
दोनों दलीलें मूल्यवान हैं, लेकिन उन्हें एक ही लेख में रखकर शीर्षक “C low-level language नहीं है” रखना थोड़ा अस्थिर लगता है। पहली दलील इस शीर्षक से मेल खाती है, और दूसरी को “machine code भी low-level language नहीं है” जैसे follow-up लेख में लेना बेहतर होता
लेकिन compile time लंबा था, और मैंने सुना है कि compiler अंततः अपेक्षित optimization level तक नहीं पहुँच पाया। x86 के साथ compatible न होना भी adoption में मददगार नहीं था
VLIW याद आता है। Wikipedia के Itanium लेख के अनुसार:
“एक VLIW instruction word में कई स्वतंत्र instructions हो सकते हैं जिन्हें dependency जाँच के बिना parallel में execute किया जा सकता है। Compiler को instructions के ऐसे valid combinations खोजने की कोशिश करनी होती है जिन्हें साथ-साथ execute किया जा सके, और असल में वह वही instruction scheduling करता है जो मौजूदा superscalar processors runtime पर hardware में करते हैं।”
अगर CPU single-flow parallelism को interface में expose करे, तो इसे compile time पर handle किया जा सकता है या inline assembly से सीधे तय भी किया जा सकता है।
मुझे जिज्ञासा है कि यह इसलिए mainstream नहीं हुआ क्योंकि industry की business dynamics ऐसी थीं, या इस strategy के सचमुच अच्छे न होने के कोई technical कारण थे।
पहला, compilers उस तरह की instruction scheduling अच्छी तरह नहीं कर पाते थे, और जब बाद में वे बेहतर हुए तब तक Itanium डूब चुका था। दूसरा, मौजूदा instruction set, यानी x86, runtime पर hardware के जरिए इसे काफ़ी अच्छी तरह करने लगा था, और असल में static scheduling से थोड़ा बेहतर नतीजे देता था। क्योंकि runtime पर profiling data उपलब्ध होता है।
Linus ने इस topic से थोड़ा जुड़ा एक अच्छा rant [0] पर लिखा था। “जब RISC वाले compiler को optimize करने में लगे थे ताकि वह सभी 32 registers को efficient तरीके से इस्तेमाल करने वाले loops बना सके, x86 implementers ने इसके बजाय chip को अलग-अलग workloads में तेज चलाने पर ध्यान दिया और जबरदस्त register renaming hardware इस्तेमाल किया। वे memory renaming भी देख रहे हैं।”
[0] https://yarchive.net/comp/linux/x86.html
VLIW कुछ niche क्षेत्रों में सचमुच अच्छी तरह काम करता है। यह in-order single instructions की तुलना में, चाहे हाथ से हो या compiler से, program करना मुश्किल है, लेकिन hardware की scheduling को simplify करता है। अगर bundled instructions की latencies मिलती-जुलती हों तो यह बेहतर काम करता है।
आज core design puzzle यह है कि memory access, arithmetic की तुलना में कहीं ज़्यादा cycles लेता है। कुछ cycles वाली arithmetic को सैकड़ों cycles वाली memory load के साथ bundle करने का ज़्यादा मतलब नहीं है। इसलिए VLIW तब अच्छी तरह काम करता है जब आपको पता हो कि memory access तेज है, यानी लगभग यह पता हो कि वह L1 cache या उसके बराबर किसी जगह में fit होगा। मुझे लगता है कि यही एक वजह है कि यह DSP-style systems के लिए अच्छा fit है।
Exposed pipelines भी ऐसे कुछ systems की दिलचस्प property हैं। अगर VLIW bundle के भीतर कोई instruction register में write करे, तो उसी register को पढ़ने वाले बाद के instructions अगले N cycles तक पुरानी value देखते हैं, और उसके बाद ही write दिखाई देता है। हाथ से program करना वाकई भ्रमित करने वाला है, लेकिन compiler ऐसी scheduling handle कर सकता है।
हाल तक DSP और HPC market का बहुत छोटा हिस्सा थे, इसलिए dynamic scheduling करने वाली architectures में ज़्यादा investment हुई और उन्होंने उन markets तक पर dominance बना लिया।
GPU में, ज़ाहिर है, स्थिति अलग हो गई, और वास्तव में GPU static scheduling पर ज़्यादा निर्भर रहे। लेकिन जैसे-जैसे GPU अधिक diverse workloads तक expand हो रहे हैं, उनमें भी धीरे-धीरे और dynamic elements आ रहे हैं।
https://news.ycombinator.com/context?id=37900987
Itanium इसे CPU के रूप में launch करने की एक प्रमुख कोशिश थी। अभी AMD64 और ARM dominate कर रहे हैं, लेकिन भविष्य में शायद हम इसे फिर देख सकते हैं।