- Rust का मौजूदा
extern "Rust"calling convention, LLVM के C calling convention path पर निर्भर करता है, और जटिल value passing में register उपयोग के मामले में काफ़ी conservative है, इसलिए बेहतर code generation के मौके छूट जाते हैं - crate-स्तर के flag
-Zcallconvके ज़रिए मौजूदाlegacyऔर नए register-केंद्रितfastmode को अलग किया जा सकता है; मुख्य विचार यह है कि optimized build में अधिक aggressive ABI का उपयोग किया जाए - LLVM में नया calling convention सीधे जोड़े बिना भी, स्थिर LLVM function signature और
poisonvalue की मदद से इस्तेमाल न होने वाले register arguments को बिना अतिरिक्त लागत के खाली छोड़ा जा सकता है और argument placement को नियंत्रित किया जा सकता है - struct, enum, union,
bool,Resultजैसे Rust types को padding छोड़कर effective size, flattening, bit packing, और stack/register split heuristics के आधार पर अधिक सघन तरीके से पास किया जा सकता है - अगर function body, borrow checker की जानकारी, और profile जानकारी को ABI निर्धारण में शामिल किया जाए तो और मजबूत optimization संभव है, लेकिन rustc के ABI code generation की जटिलता और LLVM विशेषज्ञता की कमी अभी भी व्यावहारिक बाधाएँ हैं
Rust इस समय कौन-सी calling convention optimizations छोड़ रहा है
- calling convention ABI का वह हिस्सा है जो तय करता है कि function arguments और return values कैसे पास होंगे, कौन-से registers इस्तेमाल होंगे, और prologue/epilogue तथा unwinding कैसे संभाला जाएगा
- Rust अपना unspecified calling convention परिभाषित करता है, लेकिन व्यवहार में यह LLVM के built-in C calling convention में lower हो जाता है और prologue/epilogue code generation के लिए LLVM पर निर्भर रहता है
- rustc, Clang द्वारा बनाए जा सकने वाले LLVM function signature जैसा signature बनाने की कोशिश में conservative रहता है
- इससे debugger टूटने की संभावना कम हो सकती है
- और Clang जिन ABI code generation paths का कम उपयोग करता है, उनसे जुड़े LLVM bugs को छेड़ने की संभावना भी घट सकती है
- ELF-आधारित systems में DWARF, Linux C ABI को hardcode नहीं करता, इसलिए इस लेख के दायरे में debugging compatibility को मुख्य समस्या नहीं माना गया है
- एक साधारण उदाहरण
fn extract(arr: [i32; 3]) -> i32में 12-byte array को register की बजाय pointer से पास किया जाता है- अगर
extern "C"लगाया जाए तो वही[i32; 3],rdi,rsiमें packed होकर पास होता है - यह ऐसा मामला है जहाँ Rust का default path, Linux C ABI से भी ज़्यादा conservative है
- अगर
-Zcallconv: legacy और fast को अलग करने का तरीका
extern "Rust"का मौजूदा calling convention बरकरार रखा जाता है, लेकिन crate compile flag-Zcallconvसे चुना जाता है कि कौन-सा calling convention इस्तेमाल होगा-Zcallconv=legacy: मौजूदा तरीका-Zcallconv=fast: नया डिज़ाइन किया गया register-केंद्रित तरीका-Oअपने-आप-Zcallconv=fastसेट कर सकता है
fastcalling convention, arguments को C ABI के क्रम में नहीं रखता, इसलिए x86 के पारंपरिक register order की उम्मीद रखने वालों को यह भ्रमित कर सकता है- WASM जैसे targets, जहाँ register और spilling की धारणा नहीं होती, वहाँ
-Zcallconv=fastसमर्थित न भी हो सकता है - optimization बंद वाले debug builds में
fastबदतर code बना सकता है, इसलिए उसे सक्षम करना उचित न हो - function pointers और
extern "Rust" {}blocks के लिए अलग constraints की ज़रूरत होगी- flag crate-स्तर का है, लेकिन function pointer में यह बताना कठिन है कि कौन-सा
extern "Rust"version इस्तेमाल हो रहा है - function pointer calls को धीमा और कम इस्तेमाल होने वाला path मानकर
-Zcallconv=legacyलागू किया जा सकता है - ज़रूरत पड़ने पर calling convention convert करने वाला shim बनाया जा सकता है
- unmangled symbol को call करने वाले paths की वजह से
#[no_mangle]symbols को भी legacy calling convention दिया जा सकता है
- flag crate-स्तर का है, लेकिन function pointer में यह बताना कठिन है कि कौन-सा
LLVM को परोक्ष रूप से नियंत्रित करने का तरीका
- आदर्श रूप से LLVM में सीधे यह बताना अच्छा होगा कि “यह argument इस register में जाए, यह return value उस register में जाए”, लेकिन LLVM में नया calling convention जोड़ने के लिए काफ़ी C++ code लिखना पड़ता है
- इसके बजाय, नीचे की प्रक्रिया से अपने calling convention जैसा प्रभाव हासिल किया जा सकता है
- target triple के अनुसार यह तय करना कि register से पास किए जा सकने वाले values की अधिकतम संख्या क्या होगी
- यह तय करना कि return value output registers में आएगी या
sretattribute वाले अतिरिक्तptrargument के जरिए by-reference लौटेगी - बहुत बड़े by-value arguments को by-reference में lower करना
- किन arguments को register में भेजना है यह तय करके register space utilization को अधिकतम करना
- बाकी arguments को stack पर रखना
- LLVM IR function signature को
i64,ptr,double,<2 x i64>जैसे non-aggregate arguments से बनाना - function prologue में register inputs को Rust-स्तर के arguments में decode करना
- function exit block में return value को आवश्यक output format में encode करके
retकरना - जिन non-polymorphic, non-inline functions का address लिया जा सकता है, उनके लिए function pointer identity बचाने हेतु legacy shim बनाना
- किस value को register में रखना है यह तय करना knapsack problem जैसा है, इसलिए यह NP-hard है और व्यावहारिक implementation में heuristics चाहिए
- इस जानकारी की गणना बहुत देर से करने के बजाय
rmetaमें रखा जा सकता है ताकि दोबारा गणना न करनी पड़े - Rust में हर release पर ABI बदल सकता है, इसलिए अलग-अलग Rust compilers द्वारा बने code को आपस में link न होने देना पहले से ही मौजूदा स्थिति के अनुरूप है
LLVM द्वारा अनुमत register passing की सीमाएँ
- LLVM, aggregate by-value arguments को function में पास करते समय, जहाँ तक संभव हो, उन्हें register में “explode” करने की कोशिश करता है
- x86 पर LLVM लगभग निम्न इनपुट register passing की अनुमति देता है
- 6 integer
- 8 SSE vectors
- return के लिए इसका आधा, यानी 3 integer और 4 vectors
aarch64-unknown-linuxपर input और output दोनों के लिए 8 integer और 8 vectors संभव हैं- x86 पर सभी
-Zcallconv=fastfunctions को एक ही संख्या के by-register arguments रखने लायक डिज़ाइन किया जा सकता है- integer registers के लिए 6 arguments
xmm0सेxmm7तक vector arguments के 8 slots- वास्तविक pointer passing के समय संबंधित
i64कोptrमें बदला जाएगा doublepassing के समय<2 x i64>slot को बदलेगा
- अधिकतर functions 176 bytes पास नहीं करते, फिर भी इस्तेमाल न होने वाले arguments में LLVM
poisonदेने से अतिरिक्त लागत से बचा जा सकता है- LLVM,
poisonको उस समय का सबसे सुविधाजनक value मान सकता है - अगर register argument के रूप में
poisonपास किया जाए, तो उसे “उस register में पहले से मौजूद value” की तरह माना जा सकता है, इसलिए register को छेड़ने की ज़रूरत नहीं पड़ती - उदाहरण में
load_rcx()pointer कोrcxमें लेता है, और बाकी 13 registers मेंpoisonले जाने वाला code optimization के बाद कोई code पैदा नहीं करता
- LLVM,
- यह तरीका argument passing को लगभग पूरी तरह नियंत्रित करने देता है, लेकिन input और output के लिए एक ही registers का आदर्श उपयोग architecture पर निर्भर करता है
- ARM और RISC-V, input और output में एक ही registers के उपयोग वाली संरचना के अधिक क़रीब हैं
- x86 ऐसा नहीं है, लेकिन register allocation order के बारे में अलग धारणा लेकर अनावश्यक register moves कम किए जा सकते हैं
Rust types को registers में बेहतर फिट करना
- Rust struct और union को संभालते समय यह माना जाता है कि rustc ने user types को पहले से base aggregate और union में बदल दिया है, और फिर यह तय किया जाता है कि कौन-से हिस्से registers में रखे जाएँ
- return value में struct के कुल size से ज़्यादा महत्वपूर्ण है padding छोड़कर effective size
[(u64, u32); 2]का कुल आकार 32 bytes है, लेकिन 8 bytes padding है- इसे
(u64, u32, u64, u32)में flatten करके फिर size के अनुसार(u64, u64, u32, u32)में rearrange करने पर यह 24 bytes बनता है - इसलिए यह x86 के 3 integer return registers में फिट हो सकता है
- effective size को non-
undefbits की संख्या से परिभाषित किया जाता है[(u64, u32); 2]= 192 bitsbool= 1 bitcharतकनीकी रूप से 21 bits है, लेकिन सरलता के लिए इसेu32alias जैसा माना जाता है
- जिन structs में
boolअधिक हों, उनमें कईboolको एक register में bit packing करके return किया जा सकता है - arguments वाला पक्ष अधिक कठिन है, और उसके लिए निम्न heuristics अपनाई जा सकती हैं
- जिन arguments का effective size कुल by-register input space से बड़ा हो, उन्हें by-reference में lower किया जाए
- x86 पर कुल input space 176 bytes यानी 1408 bits है
- enum को discriminant और union की जोड़ी में बदला जाए
Option<i32>को अंदरूनी रूप से(union { i32, () }, i1)की तरह देखा जा सकता हैOption<Option<i32>>को(union { i32, (), () }, i2)की तरह देखा जा सकता है
- union आम तौर पर
u8array की तरह पास किया जाए, क्योंकि uninitialized bits को मनमाने ढंग से छूना संभव है - जिन unions में केवल एक non-empty variant हो, उन्हें उसी variant से बदला जाए
- बदले गए arguments को pointer, integer, float, bool जैसे primitives में flatten किया जाए
u128,f64जैसे fields जो छोटे argument registers से बड़े हों, उन्हें split किया जा सकता है- primitive list को effective size के आधार पर sort करके, register में समा सकने वाला सबसे बड़ा prefix चुना जाए
- बाकी को stack पर रखा जाए
- जो हिस्सा stack पर जा रहा हो, अगर वह pointer size के छोटे multiple से बड़ा हो तो memory traffic घटाने के लिए उसे pointer-on-the-stack में lower किया जाए
- register से पास होने वाले values को बड़े से छोटे क्रम में रखा जाए, और
boolको प्रति register 64 तक bit-pack किया जाए
जटिल Rust function का उदाहरण और मौजूदा rustc की सीमाएँ
Option<usize>,&dyn Context,&str,[char; 6], औरOptionsstruct लेने वालेdo_thingउदाहरण में flattening और sorting के बाद सभी raw LLVM arguments register में रखे जा सकते हैं- उदाहरण के raw argument LLVM types इस रूप में बनते हैं
gprs: i64, ptr, ptr, ptr, i64, i32, i32xmm0: i32, i32, i32, i32xmm1: i32, i1, i1, i1, i1
- function prologue, primitives को निकालकर फिर Rust-स्तर के values को पुनर्निर्मित करता है
Option<usize>={ i64, i1 }- trait object =
{ ptr, ptr } &str={ ptr, i64 }[char; 6]=[6 x i32]Options={ i32, i1, i1, i1 }
- अगर argument values को वास्तव में materialize करने वाले instructions पर
!dbgmetadata लगाया जाए, तो gdb arguments print करते समय बेहतर नतीजे दे सकता है - इस समय rustc, उसी function के लिए LLVM को 8 pointer-sized parameters देता है, जिसके परिणामस्वरूप 6 integer registers तो भर जाते हैं और 2 values stack पर चली जाती हैं
return values और Result optimization की गुंजाइश
- यह डिज़ाइन, calling convention optimization के सभी संभावित रूपों को कवर नहीं करता
- कुछ मामलों में x86 के AVX registers जैसे अतिरिक्त registers का उपयोग किया जा सकता है
- struct को register और stack में बाँटकर पास करने का तरीका भी विचारणीय है
Resultreturn में अलग optimization space मौजूद है?के जरिए कई function layers से गुजरने पर अनावश्यक register moves बढ़ सकते हैं- अगर
Resultइतना बड़ा हो कि register में न समाए, तो हर?call stack में ok bit को memory से load करके जाँचना पड़ सकता है - एक विकल्प यह है कि error को out-parameter pointer में रखा जाए, और ok variant payload तथा is-ok bit को
Option<T>की तरह return किया जाए ?के साथIntocall जुड़ने वाली बारीकियाँ कठिन हैं, लेकिन लागू की जा सकती हैं
optimization-निर्भर ABI
- C के विपरीत, Rust में
-Zcallconv=fastके तहत caller को दिखने वाला ABI बनाते समय function body देखी जा सकती है - crate, हर function के लिए register passing के नज़रिए से सटीक ABI advertise कर सकता है
- सबसे सरल optimization यह है कि जो arguments इस्तेमाल ही नहीं होते उन्हें ABI से हटा दिया जाए
- अगर function किसी parameter का उपयोग नहीं करता, तो उस argument के लिए register नहीं दिया जाएगा
- अगर
&Targument को live नहीं रखा जाता, raw pointer में convert नहीं किया जाता,Tछोटा है, औरT: Freezeहै, तो reference की जगह pointee को by-value पास किया जा सकता है HashMap::get()जैसी APIs इसके उम्मीदवार हैं- अगर key का type
i32जैसा हो, तो अभी integer को stack पर spill करके उसका pointer पास करना पड़ता है - इस memory traffic से बचा जा सकता है
- अगर key का type
- profile-आधारित ABI इससे भी अधिक aggressive रूप है
- अधिक hot arguments को register allocation order में प्राथमिकता दी जा सकती है
- बड़े struct को reference से लेने पर भी उसके hot
i64fields के 3 values को caller पहले से load करके pointer और registers दोनों से पास कर सकता है - callee को वैसे भी वे loads करने होते, इसलिए अतिरिक्त लागत नहीं आती
- instrumentation profile केवल ABI में अंतर रखने वाली function cloning को भी उचित ठहरा सकता है
यह अब तक क्यों नहीं हुआ
- C++ की तुलना में Rust पर ABI constraints कम हैं, इसलिए वह बेहतर code बना सकता है, और यह विचार Go register ABI के वास्तविक उपयोग से मेल खाता है
- पहली बाधा है ABI code generation की जटिलता
- LLVM लगभग कोई उपयोगी control knobs नहीं देता
- और rustc के भीतर भी यह कोई आसान क्षेत्र नहीं है
- गलत implementation से usability पर बुरा असर पड़ सकता है
- दूसरी बाधा है विशेषज्ञता की कमी
- rustc contributors में ऐसे लोग कम हैं जो LLVM semantics और code generation characteristics को इतना गहराई से समझते हों कि अच्छा code निकाल सकें और LLVM को crash भी न करें
- compile time भी एक बोझ हो सकता है
- जैसे-जैसे function signature जटिल होंगे, LLVM को संभालने के लिए prologue/epilogue code भी बढ़ेगा
- हालांकि
-Zcallconvको केवल optimization enabled builds में इस्तेमाल करने के इरादे से देखा जा रहा है, इसलिए इसे निर्णायक कमी नहीं माना गया है
- Rust का ABI code ऐसा क्षेत्र है जहाँ bus factor कम है, और LLVM का ज्ञान Rust compiler team को अधिक optimized code बनाने में सीधे मदद कर सकता है
1 टिप्पणियां
Hacker News की राय
calling convention को optimize करते समय असली बात यह है कि दिमाग में जो रूप अच्छा लगे उसे तौलना नहीं, बल्कि performance को मापना है
code तेज होना चाहिए, सिर्फ तेज दिखना नहीं
जिसे लेखक खराब code कह रहा है, वही कभी-कभी बिल्कुल गैर-स्वाभाविक कारणों से सबसे तेज निकलता है, और यह बात बड़े benchmark में नापने पर ही पता चलती है
खराब दिखने वाला calling convention अच्छी तरह काम करने का एक कारण यह भी हो सकता है कि वह argument registers बचाता है, जिससे register allocator का काम थोड़ा आसान हो जाता है
और आज के CPU, C compiler द्वारा बनाए गए instruction flow के लिए optimize किए गए हैं, इसलिए खासकर MSVC की तरह stack passing अपेक्षा से अधिक करने वाला C-compiler शैली का code CPU के optimal point से मेल खा सकता है
inlining अब इतनी अच्छी हो गई है कि hot path में calls कम ही boundary बनते हैं, और अगर वह boundary थोड़ी गंदी भी हो लेकिन बाकी चीजों को सरल बनाती हो, तो यह ठीक है
इसका मतलब यह नहीं कि यहाँ बदलाव खराब है, लेकिन सिर्फ अजीब दिखने वाले code को देखकर बिना मापे चर्चा करना अजीब है
JavaScriptCore में calling convention optimization मेरा पेशेवर काम था, और हैरानी की बात है कि बड़े असली codebase में खराब दिखने वाला stack-passing code अक्सर जीत जाता था
लेकिन मेरा मानना है कि सिर्फ performance measurements ही एकमात्र कसौटी नहीं होने चाहिए
“आज के” CPU optimize किए गए हैं—इस वाक्य में महत्वपूर्ण शब्द “आज के” है, और CPU लगातार बदलते रहते हैं, इसलिए calling convention को long-term design होना चाहिए
इसलिए दुर्भाग्य से C++ के तरीके से बहुत दूर न जाना ही फायदेमंद है, क्योंकि भविष्य के processor optimizations भी संभवतः उसी दिशा को लक्ष्य करेंगे
साथ ही argument registers बचाने जैसे ऐसे सामान्य सिद्धांतों को ध्यान में रखना अच्छा है जो आसानी से नहीं बदलते, ताकि calling convention मजबूत और भविष्य-उन्मुख बन सके
पिछले कुछ वर्षों में Rust, strangeness budget(https://steveklabnik.com/writing/the-language-strangeness-bu...) के मामले में शायद बहुत ज़्यादा conservative हो गया है, इसलिए यह कहना थोड़ा अजीब लगता है। आखिर अलग हुए बिना बेहतर नहीं हुआ जा सकता
अगर function शुरू होते ही parameter का address लेकर उसे किसी अज्ञात function को दे दे, तो आखिरकार उसे stack पर ही ले जाना पड़ेगा
function-body आधारित calling convention optimization देखना दिलचस्प होगा। C के static function के लिए, जब तक उसका address नहीं लिया जाता, यह सुरक्षित लगता है
JIT, assembly की एक पंक्ति बनाने से पहले ही वास्तविक चल रहे CPU के बारे में काफी जानकारी जुटा चुका होता है, इसलिए इस समस्या में उसे फायदा मिलता है
शुद्ध static compiled code में runtime के architecture feature set का पता नहीं होता, इसलिए जिन code paths को सबसे अधिक optimize करना होता है, वहीं अक्सर inlining barriers मिल जाते हैं
अभी Rust छोटे platforms पर इस मामले में कमजोर दिखता है, और calling convention
Resultreturn के संदर्भ में मददगार हो सकता हैफिर भी यह जानने की जिज्ञासा है कि stack passing के empirical फायदे, ज़्यादा registers वाले ARMV8 CPU या RISC-V पर भी उतने ही लागू होते हैं या नहीं
यह एक उचित ड्राफ्ट है, लेकिन इसमें caller-saved/callee-saved का भेद गायब है, और input registers के कुछ हिस्सों को output के लिए असाइन करने वाली एक आम गलती भी है
यह उम्मीद करना भी कुछ ज़्यादा आशावादी है कि debugger, C से अलग calling convention को समझ लेगा। DWARF सैद्धांतिक रूप से जो कुछ भी encode कर सकता हो, व्यवहार में उसके बुरी तरह विफल होने की संभावना बहुत ज़्यादा है
optimization settings के आधार पर ABI बदलना separate compilation के साथ बहुत बुरी तरह interact करता है
arguments को empty packing की तरह rearrange करने का तरीका काम तो कर सकता है, लेकिन इससे compiler की complexity बहुत बढ़ेगी, और left-to-right first-fit placement की तुलना में यह वास्तव में कितना उपयोगी है, यह स्पष्ट नहीं है। इससे developers के लिए यह अनुमान लगाना भी कठिन हो जाएगा कि arguments कहाँ जाएंगे
जिन functions में address escape होता है और जिनमें नहीं होता, उनके लिए अलग calling conventions रखना एक बड़े स्तर पर उचित दिशा लगती है। impedance matching करने वाले prologue को अलग कर देना भी अच्छी तरह काम करता है
Rust को C से अलग calling convention रखने की इच्छा होनी चाहिए, लेकिन क्या वह हर function के लिए इस्तेमाल होने वाला hardcoded एकमात्र convention होना चाहिए, यह स्पष्ट नहीं है। इसे type system में रखना अधिक स्वाभाविक लगता है, और अगर developers को calling convention नियंत्रित करने दिया जाए तो assembly के performance फ़ायदों में से एक कम हो जाता है
caller के नज़रिए से तो वैसे भी दो function calls के बीच output registers खाली करने पड़ते हैं, और system call conventions में भी यह काफ़ी व्यापक रूप से इस्तेमाल होता है
शायद मकसद यह है कि callee, input values को जस का तस रखते हुए output values आसानी से तैयार कर सके। अगर ऐसा है, तो output registers को input क्रम के अंत में रखकर overlap से बचने की कोशिश समझ आती है, लेकिन हर तरह के overlap को पूरी तरह प्रतिबंधित करने की वजह स्पष्ट नहीं लगती
Function Aद्वाराFunction B,Function C,Function Dको कॉल करने जैसी शृंखला में बीच के functions के arguments को दूसरे convention में बदलकर overhead घटाने वाली optimizations भी साथ ही रुक जाएँगीयह सवाल है कि ऐसी कौन-सी semantics हो सकती है जो उस optimization को भी बचाए रखे और नियंत्रण भी दे, या फिर क्या यह विचार मूलतः एक भ्रम ही है
वास्तव में assembly आम तौर पर अधिकांश compiler optimizations का लक्ष्य नहीं होती, इसलिए performance के लिहाज़ से उसका नुकसान होता है। उसे अक्सर “व्यवहार देखकर यह तय कर लेना कि यह पूरी तरह redundant है और इसे पूरा हटा देना” जैसी optimization भी नहीं मिलती, और अब 1990 का दशक नहीं है
हालाँकि जहाँ ऐसी optimizations पर विचार करना भी संभव नहीं है, वहाँ inline assembly की तुलना में स्पष्ट बढ़त शायद केवल profile-guided optimization की होगी। क्योंकि application developer को code के व्यवहार का पूरा ज्ञान होता है और compiler developer को नहीं
call overhead को तब तक और assembly लिखकर हटाया जा सकता है जब तक वह संबंधित hot boundaries को कवर न कर ले
boolके मामले में यह dependency chain बना सकती हैx64 पर
boolvalues के लिए पहले उन्हें registers में डालना, फिर shift करना, और result में OR करना—इससे बेहतर तरीका साफ़ नहीं दिखतासीधा तरीका 64 लंबी dependency chain बनाता है और 64-cycle penalty दे सकता है, हालांकि अच्छे implementation से इसे 6 cycles तक, और व्यावहारिक रूप से शायद 12 cycles तक घटाया जा सकता है
लेकिन यह भी सवाल है कि 64
boolआएँगे कहाँ से। इतने registers होते नहीं, इसलिए आखिरकार उन्हें stack से फिर पढ़ना पड़ेगाअगर Rust ABI पहले से ही structs के अंदर
boolको इसी तरह सघन रूप से pack करता है, तो यह काम वैसे भी करना पड़ेगा, लेकिन इस बारे में निश्चित नहीं हूँऔर caller को फिर सब कुछ दोबारा unpack भी करना होगा
compiler को values को stack के result space में flow करने देना सिखाना शायद आसान होगा, और संभव है कि performance भी बेहतर मिले
ऐसे में यह सवाल उठता है कि values को registers में डालने से वास्तव में कितना फ़ायदा मिलता है
C calling convention कुछ खास अच्छी नहीं है
यह सच है कि C calling convention बदली नहीं जा सकती, लेकिन इससे वह कम खटकने वाली नहीं हो जाती
सभी उपलब्ध caller-saved registers को arguments और return values के लिए इस्तेमाल होना चाहिए, जबकि पारंपरिक SysV ABI में return value के लिए केवल एक register, और कभी-कभी दो, इस्तेमाल होते हैं
struct Point3D { long x, y, z }लौटाते समयPoint3Dकोrax,rdi,rsiमें रखा जा सकता है, फिर भी उसे stack पर spill किया जाता हैदूसरे systems में दूसरी तरकीबें भी हैं। अगर ठीक याद है, तो SBCL में जब function कई values return करता है तो exit पर carry flag set किया जाता है। उदाहरण के लिए,
Resultमें error है या नहीं यह दिखाने के लिए carry flag का इस्तेमाल अच्छा हो सकता हैC calling convention मूलतः उसी चीज़ को support करती है जिसे C support करता है, यानी एक argument return करना। struct return भी वास्तव में ठीक से नहीं
C में यह लगभग “तुम्हें यह पता नहीं था क्या” जैसा है, और C++ की ओर यह “बस inline कर दो” वाले रवैये में बदल जाता है
दूसरी ओर, memory spill वास्तव में होता है। उदाहरण के लिए, SPARC के उदार register space और windows साधारण functions में बहुत-से unused registers छोड़ देते थे, और register ring को spill करने पर cache को बिगाड़ देने वाला बड़ा stack usage हो जाता था
x86 पर डेटा को “जहाँ ज़रूरत है” वहाँ ले जाने वाले बहुत-से
movहोने के बावजूद, कुल मिलाकर कई बार वह तेज़ निकलता थाकेवल callee code को देखकर यह कहना आसान लगता है कि “यह argument यहाँ हो और वह return value वहाँ हो तो निश्चित ही तेज़ होगा,” लेकिन caller के बारे में कुछ पता नहीं होता
यह गारंटी नहीं दी जा सकती कि argument preparation वैसे ही आगे पास हो जाएगी, या return value तुरंत hot path में consume होगी। उदाहरण के लिए, अगर
struct Point { x: i32, y: i32, z: i32 }को argument/return के रूप में इस्तेमाल किया जा रहा हो और caller किसी loop मेंmystruct.deepinside.point[i] = func(mystruct.deepinside.point[i])जैसा कुछ कर रहा हो, तो registers में डालना-निकालना overhead बन सकता है या vectorization को रोक सकता हैcallee यह नहीं जान सकता, और अपवाद केवल तब है जब compiler दोनों पक्षों को देखकर inline कर सके
calls से जुड़ा सबसे आसान और स्पष्ट सुधार शायद लगभग सभी C ABI में जड़ी हुई यह धारणा हटाना है कि function एक ही primitive value return करता है। बाकी सबके लिए बहुत-से benchmarks और code generation statistics की ज़रूरत होगी
Rust में एक और निराशाजनक बारीकी है, जहाँ struct आपकी अपेक्षा से बड़ा हो जाता है
अगर
Foostruct में 8Optionfield हों, जिनमें हर एकNoneयाSome(u8)हो, तो C में इसे 1-बिटboolके 8 मान और 8uint8_tके साथ कुल 9 bytes में दर्शाया जा सकता हैRust में यह 1-byte discriminator और
uint8_tके 8 दोहराव के कारण 16 bytes हो जाता हैवजह यह है कि struct को अपने fields के borrow उपलब्ध कराने में सक्षम होना चाहिए। अगर आपके पास
&Fooहै, तो compiler को&Foo::some_field, यानी&Option, बना पाना चाहिए, और इस&Optionका रूप प्रोग्राम के बाकी सभी&Optionजैसा ही होना चाहिएइसलिए अंदर का
Optionभी प्रोग्राम के दूसरेOptionजैसा ही layout रखता है, यानी अपने discriminator bit को byte तक round up करके, साथ मेंu8के साथ। भले ही आप वास्तव में&Foo::some_fieldकभी न बनाएं, struct को यह लागत फिर भी चुकानी पड़ती हैबड़े types के
Optionके साथ स्थिति और खराब होती है। 8Optionfields वाले struct में हर discriminator 2 bytes तक round up हो जाता है, जिससे कुल 32 bytes हो जाते हैं, और लगभग एक-चौथाई—अगर discriminator के unused bits भी जोड़ें तो लगभग आधा—मध्य padding में व्यर्थ चला जाता है। इसका C समकक्ष सिर्फ 18 bytes में हो सकता हैOptionइस्तेमाल करने पर Rust struct 128 bytes का हो सकता है, जबकि C struct 72 bytes काबेशक, packed discriminators के लिए एक
u8और 8MaybeUninitरखकर, तथा&FooसेOption<&T>और&mut FooसेOption<&mut T>में map करने वाले functions खुद लिखकर, C जैसा representation लागू किया जा सकता है। बस आप इसे&Optionया&mut Optionके रूप में उपयोग नहीं कर पाएंगेhttps://play.rust-lang.org/?version=stable&mode=debug&editio...
असल में आपने 8
Optionरखने वाले एक user-defined type का वर्णन किया है, और जैसे ही performance की चिंता शुरू होती है, internalOptionhandling खुद करनी पड़ती हैसिर्फ इसलिए कि Rust target के हिसाब से काम आने पर इस्तेमाल करने लायक convenient features देता है, इसे कमी मानना मुश्किल है
बताया गया use case अपेक्षाकृत दुर्लभ है, और अगर यह सचमुच performance bottleneck हो, तो Rust में थोड़ा अधिक समय देकर implement करना कोई बड़ी समस्या नहीं है
सामान्य उपयोग में
Option<_>type के फायदे इतने बड़े हैं कि इसे Rust की “निराशाजनक बारीकी” कहना कठिन हैnon-polymorphic, non-inline functions का address अगर function pointer के रूप में लिया जा सकता है, तो
-Zcallconv=legacyवाला shim बनाकर असली implementation को तुरंत tail-call करने की बात कही गई है; function pointer equivalence बनाए रखने का इरादा समझ में आता हैलेकिन अगर legacy shim किसी Rust calling convention function को tail-call करे, तो calling convention के return value difference को ठीक नहीं किया जा सकेगा, है न?
थोड़ा अलग विषय है, लेकिन जानना चाहता हूँ कि क्या अभी Go और Rust interop संभव है
मुझे याद है कि पहले किसी ने बीच में Zig रखकर यह किया था, लेकिन अब वह उदाहरण मिल नहीं रहा। मेरे पास कुछ legacy Rust code है, जिसे मैं धीरे-धीरे Go में migrate करना चाहता हूँ
extern "C"FFI इस्तेमाल करके Rust functions को call किया जा सकता हैGitHub code search में यह कैसे किया, इस पर RustConf 2023 में एक talk भी थी(https://www.youtube.com/watch?v=KYdlqhb267c), और बाद में सुना कि 1Password जैसी जगहों पर भी ऐसा ही किया जा रहा है
C interop boundary के पार types को ले जाना काफ़ी झंझटभरा होता है, इसलिए यह बहुत मज़ेदार नहीं है, लेकिन संभव है और code reuse भी किया जा सकता है
extern "C"के रूप में declare करें, फिर Go से वैसे ही call करें जैसे C को करते हैंउल्टी दिशा के बारे में मुझे ज़्यादा नहीं पता
managed code को उस memory का ownership चाहिए जिसे वह free या move कर सके, और unmanaged code को यह समझना पड़ता है कि memory कब free होगी या move होगी
cgoजैसी चीज़ें Go के managed code से unmanaged memory में FFI calls को मिलाने देती हैं, लेकिन इसकी कीमत चुकानी पड़ती हैजिन implementations में एक-दूसरे को call करने वाली भाषाएँ garbage collector share नहीं करतीं, वहाँ यह समस्या हमेशा आती है
managed/unmanaged code को मिलाना एक पुराना विचार है, लेकिन इस पर आज भी सक्रिय research हो रही है
अगर built-in runtime इसके लिए डिज़ाइन न किया गया हो, तो unmanaged code से managed code को call करना लगभग हमेशा बुरा विचार होता है, और आमतौर पर बीच में serialization layer रखी जाती है
अगर यही आपका मुख्य काम हो तो यह खराब लग सकता है, लेकिन मैं इस बात से थक गया था कि हर कुछ हफ्तों बाद code पर लौटने पर याद ही नहीं रहता था कि क्या और कैसे करना है
मैंने stateful Rust closure को callback के रूप में Go code को दिया, ताकि उसे Go standard library function में डाला जा सके, और इसमें Rust closure के अंदर होने वाला panic unwinding भी शामिल था
https://github.com/Voultapher/sort-research-rs/commit/df6c91...
सेक्शन का शीर्षक तिरछा कैसे सेट किया गया है, यह पता लगाने के लिए मैंने काफ़ी देर तक Inspect Element देखा, लेकिन Safari tools के हिसाब से समझ नहीं आया। आखिर यह किया कैसे गया है?
.post-titleelement पर है:transform: skewY(-2deg) translate(-1rem, -0.4rem);element()function(https://developer.mozilla.org/en-US/docs/Web/CSS/element) इस्तेमाल कर रहा है, लेकिन असल में वह लेख के मुख्य भाग की बहुत छोटी करके बनाई गई एक कॉपी थीh1, h2, h3, h4, h5, h6परtransform:skewY(-2deg) translate(-1rem,0rem);,transform-origin:top;,font-style:italic;,text-decoration-line:underline;,text-decoration-color:goldenrod;,text-underline-offset:4%;,text-decoration-thickness:.25exलागू हैइसके उलट 2019 का लेख “How Swift Achieved Dynamic Linking Where Rust Couldn't” है
https://faultlore.com/blah/swift-abi/
यह अफ़सोस की बात है कि Rust के पास अभी तक Rust-स्तरीय semantics के लिए calling convention नहीं है, लेकिन साथ ही वह लेख यह भी दिखाता है कि वहाँ तक पहुँचने के लिए काम की मात्रा कितनी ज़बरदस्त है
Apple के पास Swift को ऐसी व्यावहारिक system language बनाने की गहरी प्रेरणा थी जिस पर applications निर्भर कर सकें, लेकिन Rust को ऐसा समर्थन नहीं मिला है
HN चर्चा: https://news.ycombinator.com/item?id=21488415
अच्छा होगा अगर Rust में भी इस trade-off के लिए support options ज़्यादा हों, और वे सिर्फ़ https://github.com/rust-lang/rfcs/pull/3470 जैसी चीज़ों तक सीमित न रहें
अगर मौजूदा Rust compiler बहुत aggressively inline करके फिर optimize करता है, तो सवाल है कि क्या यह मेहनत वाकई वर्थ है
अगर call किया जाने वाला function छोटा है, तो वह inline हो जाएगा, और अगर बड़ा है, तो function के अंदर ही काफ़ी समय लगेगा, इसलिए call overhead छोटा होगा
dyn Trait, inline नहीं किए जा सकते, इसलिए इस तरह का बदलाव मददगार होगाअगर calls को सस्ता बनाया जा सके, तो इतनी aggressively inline करने की ज़रूरत नहीं होगी, जिससे code size और compile time में भी मदद मिल सकती है
जो complex functions inline के लिए उपयुक्त नहीं हैं, वे संभवतः memory को कई बार access करेंगे, और वही access bottleneck होने की संभावना है
stack-passing उस bottleneck को और कस देता है, क्योंकि इससे cache pressure और load/store बढ़ते हैं
अगर Rust काफ़ी बड़े हिस्से के function calls में arguments को optimal तरीके से pass कर सके, तो वह सिर्फ़ L1 access के कुछ cycles ही नहीं बचाएगा, बल्कि CPU को असली memory bottleneck तक और जल्दी पहुँचा सकता है
शायद इससे कुछ प्रतिशत का फ़ायदा हो, लेकिन अभी मैं वाइन पी रहा हूँ और हिसाब नहीं लगा रहा
x86 reference material में आने वाले “Diana’s silk dress cost $89” mnemonic का मतलब कोई समझा सकता है?