यह लेख Rust भाषा की Calling Convention को बेहतर बनाने के तरीकों को विस्तार से समझाता है
Rust की मौजूदा Calling Convention की समस्याएँ
- Rust में अभी calling convention स्पष्ट रूप से परिभाषित नहीं है
- व्यवहार में यह LLVM की default C calling convention का उपयोग करता है
- Rust अभी सावधानी से ऐसे LLVM function signatures बनाने की कोशिश करता है, जैसे Clang बनाता
- debugger के साथ compatibility के लिए
- LLVM bugs से बचने के लिए
- लेकिन यह ज़रूरत से ज़्यादा conservative है, इसलिए साधारण functions के लिए भी खराब code बनता है
fn extract(arr: [i32; 3]) -> i32 { arr[1] }
- ऊपर वाला code registers के जरिए जाना चाहिए, लेकिन pointer से पास होता है
- Rust, C ABI से भी ज़्यादा conservative है.
extern "C"देने पर यह register से पास होता है.
नई Calling Convention का प्रस्ताव
extern "Rust"functions के लिए मौजूदा calling convention बनाए रखी जाए-Zcallconvflag जोड़ा जाए ताकिextern "Rust"functions की calling convention सेट की जा सके-Zcallconv=legacyमौजूदा तरीका है-Zcallconv=fastनया डिज़ाइन किया जाने वाला तरीका है
- मौजूदा calling convention क्यों बनाए रखनी चाहिए?
- debugging को आसान रखने के लिए इसे C ABI क्रम में नहीं रखा जाता
- WASM जैसे कुछ targets इसे support नहीं कर सकते
- debug builds में इसका कोई मतलब न हो सकता है
- function pointers और
extern "Rust" {}blocks से जुड़ी सावधानियाँ- यह crate-level flag है, इसलिए function pointers पर लागू नहीं किया जा सकता
- function pointer calls धीमे और कम होते हैं, इसलिए
-Zcallconv=legacyइस्तेमाल किया जाए - ज़रूरत पड़ने पर calling convention बदलने के लिए shim बनाया जाए
extern "Rust" { fn my_func() -> i32; }की तरह सीधे call करने पर- सिर्फ non-mangled symbols को call किया जा सकता है
#[no_mangle]functions पुरानी calling convention का उपयोग करेंगी
LLVM का उपयोग कैसे किया जा सकता है
- आदर्श रूप से LLVM में calling convention सीधे specify की जा सके तो अच्छा होगा, लेकिन व्यवहार में यह मुश्किल है
- इसे नीचे दिए गए तरीके से workaround किया जा सकता है
- दिए गए target के लिए register से पास हो सकने वाली अधिकतम values की संख्या पता करें
- return value कैसे पास होगी, यह तय करें. अगर register में फिट हो तो वैसे ही, नहीं तो reference से पास करें
- value के रूप में पास किए गए arguments में से किन्हें reference से पास करना है, यह चुनें
- जो register से पास होने वाली space से बड़े हों
- x86 पर यह लगभग 176 bytes है
- register space का अधिकतम उपयोग करने के लिए तय करें कि कौन से arguments register से जाएँगे
- यह NP-hard समस्या है, इसलिए heuristic चाहिए
- बाकी stack से पास किए जाएँ
- LLVM IR में function signature बनाएँ
- register से पास होने वाले arguments को i64, ptr, double, <2 x i64> जैसी non-aggregate representations में दिखाया जाए
- stack से पास होने वाले arguments "register inputs" का अनुसरण करें
- function prologue बनाएँ
- Rust-level arguments को register inputs से decode करके वही
%ssavalues बनाई जाएँ जो-Zcallconv=legacyमें बनती हैं - function body के लिए calling convention से स्वतंत्र होकर वही code generation संभव है
- अनावश्यक decoding code, DCE से हट जाएगा
- Rust-level arguments को register inputs से decode करके वही
- function return block बनाएँ
-Zcallconv=legacyजैसी ही return types के लिए phi instructions शामिल हों- ज़रूरी output format में encode करके
retसे लौटाया जाए retकी जगह इस block में branch करना होगा
- अगर कोई non-polymorphic, non-inline function है जिसे function pointer के रूप में इस्तेमाल किया जा सकता है
- crate के बाहर expose किया गया हो या function pointer के रूप में पास किया जाता हो
- तो
-Zcallconv=legacyइस्तेमाल करने वाला shim बनाया जाए और असली implementation को Tail Call किया जाए - function pointer equality बनाए रखने के लिए यह ज़रूरी है
LLVM की register passing limits पता करने का तरीका
- LLVM program जो LLVM द्वारा अनुमति दी गई register passing की अधिकतम संख्या पता करता है
- x86 पर 6 integer, 8 SSE vector inputs और 3 integer, 4 SSE vector outputs संभव हैं
- aarch64 पर 8 integer, 8 vector inputs और outputs दोनों के लिए समान हैं
- इससे आगे जाने पर values stack पर पास होती हैं
Rust में structs और enums की handling
- माना गया है कि rustc पहले ही इन्हें primitive aggregates और unions में बदल चुका है
- return value handling
- struct का size नहीं, बल्कि padding हटाने के बाद actual data size महत्वपूर्ण है
- [(u64, u32); 2] 32 bytes है, लेकिन 8 bytes padding हटाने पर 24 bytes रह जाता है
- type का effective size परिभाषित किया जाता है
- padding को छोड़कर undefined bits की संख्या
- [(u64, u32); 2] 192 bits है
- bool 1 bit है
- अगर effective size output register space से कम है, तो value के रूप में return करें
- x86 पर 3 integers + 4 SSE = 88 bytes = 704 bits
- argument registers की handling
- यह Knapsack समस्या है, इसलिए NP-hard है
- एक सरल heuristic
- अगर effective size कुल input register space से बड़ा है, तो reference से पास करें
- enum को discriminant-union pair से बदलें
- union uninitialized bits को छू सकता है, इसलिए इसे
u8array या किसी एक non-empty variant के रूप में पास करें - pointers, integers, floating point, booleans आदि सबसे बुनियादी elements तक flatten करें
- effective size के आधार पर ascending order में sort करें
- जितना बड़ा prefix संभव हो उसे registers को दें, बाकी stack पर भेजें
- अगर stack पर जाने वाले input का कोई हिस्सा pointer size के छोटे multiple से बड़ा है, तो stack के pointer से पास करें
- बाकी को sort से पहले वाले क्रम में सीधे stack पर पास करें
- register से जाने वाली चीज़ों को size के descending order में assign करें
- booleans को 64-64 के समूह में bit-pack करें
GN+ की राय
- व्यक्तिगत रूप से, Rust की मौजूदा calling convention काफी निराशाजनक लगती है. इसमें C++ से कहीं बेहतर performance देने की क्षमता है, लेकिन अभी तक ऐसा नहीं हो पा रहा
- Go भाषा ने यह तरीका बहुत पहले ही लागू कर लिया था
- Rust के इसे लागू न कर पाने के कारण
- ABI code generation जटिल है और LLVM इसमें ज़्यादा मदद नहीं करता
- compiler team में LLVM को अच्छी तरह जानने वाले लोग बहुत कम हैं
- compile time को लेकर चिंता है, लेकिन इसे सिर्फ optimized builds में इस्तेमाल किया जाएगा, इसलिए यह बड़ी समस्या नहीं है
- लेखक के पास इसे खुद ठीक करने का समय नहीं है, लेकिन LLVM की विशेषज्ञता के आधार पर वह Rust compiler team की मदद करने के इच्छुक हैं
- या फिर सीधे
extern "C"याextern "fastcall"पर जाना भी एक विकल्प हो सकता है
1 टिप्पणियां
Hacker News राय
सारांश:
Option<u8>fields वाला struct, Rust में 16 bytes और C में 9 bytes का होता है।&Option<T>या&mut Option<T>से map नहीं किया जा सकता।reprसे बंद किया जा सकता है।