c++ - T*को रजिस्टर में पास क्यों किया जा सकता है, लेकिन एक विशिष्ट_<t> नहीं हो सकता है?




assembly unique-ptr (2)

मैं CppCon 2019 में चैंडलर कारुथ की बात देख रहा हूं:

कोई शून्य लागत सार नहीं हैं

इसमें, वह उदाहरण देता है कि कैसे वह आश्चर्यचकित था कि आपने एक std::unique_ptr<int> एक int* का उपयोग करके कितना उपरिव्यय किया है; वह खंड समय बिंदु 17:25 पर शुरू होता है।

आप उनके उदाहरण जोड़ी-ऑफ-स्निपेट्स (Godbolt.org) के संकलन परिणामों पर एक नज़र डाल सकते हैं - साक्षी के रूप में, वास्तव में, ऐसा लगता है कि संकलक यूनिक_प्रेट वैल्यू को पारित करने के लिए तैयार नहीं है - जो वास्तव में नीचे की रेखा में है। सिर्फ एक पता - एक रजिस्टर के अंदर, केवल सीधे मेमोरी में।

श्री कैर्र्थ ने लगभग 27:00 पर जो अंक बनाए हैं उनमें से एक यह है कि C ++ ABI को मेमोरी में पास होने के लिए (कुछ नहीं बल्कि सभी - शायद - नॉन-प्रिमिविटिव टाइप? नॉन-ट्रिवियल-कंस्ट्रक्टेबल टाइप?) की आवश्यकता होती है। बल्कि एक रजिस्टर के भीतर।

मेरे सवाल:

  1. क्या यह वास्तव में कुछ प्लेटफार्मों पर एक एबीआई आवश्यकता है? (जो?) या शायद यह कुछ परिदृश्यों में केवल कुछ निराशा है?
  2. एबीआई ऐसा क्यों है? यही है, अगर एक संरचना / वर्ग के क्षेत्र रजिस्टर के भीतर फिट होते हैं, या एक भी रजिस्टर - तो हमें उस रजिस्टर के भीतर पारित करने में सक्षम क्यों नहीं होना चाहिए?
  3. क्या C ++ मानकों की समिति ने हाल के वर्षों में इस बिंदु पर चर्चा की है, या कभी?

पुनश्च - ताकि इस प्रश्न को बिना कोड के न छोड़ा जाए:

सादा सूचक:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

अद्वितीय सूचक:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}

  1. क्या यह वास्तव में एक ABI आवश्यकता है, या शायद यह कुछ परिदृश्यों में केवल कुछ निराशा है?

एक उदाहरण सिस्टम वी एप्लीकेशन बाइनरी इंटरफ़ेस AMD64 आर्किटेक्चर प्रोसेसर सप्लीमेंट है । यह ABI 64-बिट x86- संगत CPU (Linux x86_64 Architectecure) के लिए है। यह Solaris, Linux, FreeBSD, macOS, Linux के लिए Windows सबसिस्टम पर अनुसरण किया जाता है:

यदि C ++ ऑब्जेक्ट में या तो एक गैर-तुच्छ कॉपी निर्माता या एक गैर-तुच्छ विध्वंसक है, तो इसे अदृश्य संदर्भ द्वारा पास किया जाता है (ऑब्जेक्ट को पैरामीटर सूची में एक संकेतक द्वारा प्रतिस्थापित किया जाता है जिसमें वर्ग INTEGER होता है)।

एक गैर-तुच्छ कॉपी निर्माता या एक गैर-तुच्छ विध्वंसक के साथ एक वस्तु को मूल्य से पारित नहीं किया जा सकता है क्योंकि ऐसी वस्तुओं में अच्छी तरह से परिभाषित पते होने चाहिए। किसी फ़ंक्शन से ऑब्जेक्ट वापस करते समय समान समस्याएं लागू होती हैं।

ध्यान दें, कि केवल 2 सामान्य प्रयोजन के रजिस्टरों का उपयोग 1 वस्तु को एक ट्रिवियल कॉपी कंस्ट्रक्टर और एक ट्रिवियल डिस्ट्रक्टर के साथ पास करने के लिए किया जा सकता है, अर्थात केवल 16 से अधिक sizeof की वस्तुओं को रजिस्टरों में पारित नहीं किया जा सकता है। विशेष रूप से ,7.1 पासिंग और लौटने वाली वस्तुओं में कॉलिंग सम्मेलनों के विस्तृत उपचार के लिए एग्नर फॉग द्वारा कॉलिंग कन्वेंशन देखें। रजिस्टरों में SIMD प्रकारों को पास करने के लिए अलग-अलग कॉलिंग कन्वेंशन हैं।

अन्य CPU आर्किटेक्चर के लिए अलग-अलग ABI हैं।

  1. एबीआई ऐसा क्यों है? यही है, अगर एक संरचना / वर्ग के क्षेत्र रजिस्टर के भीतर फिट होते हैं, या एक भी रजिस्टर - तो हमें उस रजिस्टर के भीतर पारित करने में सक्षम क्यों नहीं होना चाहिए?

यह एक कार्यान्वयन विवरण है, लेकिन जब स्टैक अनइंडिंग के दौरान एक अपवाद को संभाला जाता है, तो स्वचालित भंडारण अवधि नष्ट होने वाली वस्तुओं को फ़ंक्शन स्टैक फ्रेम के सापेक्ष पता होना चाहिए क्योंकि रजिस्टरों को उस समय तक बंद कर दिया गया है। स्टैक अनइंडिंग कोड को अपने डिस्ट्रक्टर्स को इनवॉइस करने के लिए ऑब्जेक्ट्स के पते की आवश्यकता होती है लेकिन रजिस्टरों में ऑब्जेक्ट्स का एड्रेस नहीं होता है।

मूल रूप से, विनाशकारी वस्तुओं पर काम करते हैं :

एक वस्तु अपने निर्माण की अवधि ([class.cdtor]), अपने पूरे जीवनकाल में, और अपने विनाश की अवधि में भंडारण के क्षेत्र में रहती है।

और कोई वस्तु C ++ में मौजूद नहीं हो सकती है यदि इसके लिए कोई पता योग्य भंडारण आवंटित नहीं किया जाता है क्योंकि ऑब्जेक्ट की पहचान उसका पता है

जब रजिस्टरों में रखे गए एक तुच्छ कॉपी निर्माता के साथ किसी ऑब्जेक्ट का पता आवश्यक होता है, तो कंपाइलर ऑब्जेक्ट को मेमोरी में स्टोर कर सकता है और एड्रेस प्राप्त कर सकता है। यदि दूसरी ओर कॉपी कंस्ट्रक्टर गैर-तुच्छ है, तो कंपाइलर इसे मेमोरी में स्टोर नहीं कर सकता है, इसे कॉपी कंस्ट्रक्टर को कॉल करने की आवश्यकता होती है, जो एक संदर्भ लेता है और इसलिए रजिस्टरों में ऑब्जेक्ट के पते की आवश्यकता होती है। कॉलिंग कन्वेंशन संभवत: इस बात पर निर्भर नहीं कर सकता है कि कॉपी कंस्ट्रक्टर कैली में इनबिल्ट था या नहीं।

इस बारे में सोचने का एक और तरीका यह है कि तुच्छ रूप से प्रतिलिपि योग्य प्रकारों के लिए कंपाइलर रजिस्टरों में किसी ऑब्जेक्ट के मूल्य को स्थानांतरित करता है, जिसमें से यदि आवश्यक हो तो एक ऑब्जेक्ट को प्लेन मेमोरी स्टोर्स द्वारा पुनर्प्राप्त किया जा सकता है। उदाहरण के लिए:

void f(long*);
void g(long a) { f(&a); }

सिस्टम V ABI के साथ x86_64 पर:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

अपने विचार-विमर्श में चांडलर कारूथ mentions कि विनाशकारी चाल को लागू करने के लिए एक टूटी हुई एबीआई परिवर्तन (अन्य चीजों के बीच) आवश्यक हो सकता है जो चीजों को बेहतर बना सकता है। IMO, ABI परिवर्तन गैर-ब्रेकिंग हो सकता है यदि नए ABI का उपयोग करने वाले फ़ंक्शंस स्पष्ट रूप से एक नए भिन्न लिंकेज का उपयोग करते हैं, उदाहरण के लिए उन्हें extern "C++20" {} ब्लॉक में घोषित करें (संभवतः, एक नई इनलाइन स्थान में मौजूदा API को माइग्रेट करने के लिए)। ताकि नए लिंकेज के साथ नए फ़ंक्शन की घोषणाओं के खिलाफ संकलित केवल कोड नए एबीआई का उपयोग कर सके।

ध्यान दें कि ABI तब लागू नहीं होता है जब बुलाया फ़ंक्शन को इनलाइन किया गया हो। लिंक-टाइम कोड जेनरेशन के साथ-साथ कंपाइलर अन्य ट्रांसलेशन यूनिट में परिभाषित इनलाइन फ़ंक्शन को भी कर सकता है या कस्टम कॉलिंग कन्वेंशन का उपयोग कर सकता है।


क्या यह वास्तव में कुछ प्लेटफार्मों पर एक एबीआई आवश्यकता है? (जो?) या शायद यह कुछ परिदृश्यों में केवल कुछ निराशा है?

यदि कंप्लीशन यूनिट बाउंड्री पर कुछ दिखाई दे रहा है तो क्या यह स्पष्ट रूप से परिभाषित है या स्पष्ट रूप से यह एबीआई का हिस्सा है।

एबीआई ऐसा क्यों है?

मूलभूत समस्या यह है कि रजिस्टर बच जाते हैं और हर समय बहाल हो जाते हैं क्योंकि आप कॉल स्टैक नीचे और ऊपर जाते हैं। इसलिए उनका संदर्भ या सूचक होना व्यावहारिक नहीं है।

इन-लाइनिंग और इससे होने वाले ऑप्टिमाइज़ेशन अच्छा होता है जब ऐसा होता है, लेकिन एक एबीआई डिज़ाइनर ऐसा होने पर भरोसा नहीं कर सकता है। उन्हें एबीआई को सबसे खराब स्थिति मानते हुए डिजाइन करना होगा। मुझे नहीं लगता कि प्रोग्रामर एक कंपाइलर के साथ बहुत खुश होंगे जहां अनुकूलन स्तर के आधार पर एबीआई बदल गया।

एक तुच्छ रूप से कॉपी करने योग्य प्रकार को रजिस्टरों में पारित किया जा सकता है क्योंकि तार्किक कॉपी ऑपरेशन को दो भागों में विभाजित किया जा सकता है। मापदंडों को कॉलर द्वारा पासिंग पैरामीटर के लिए उपयोग किए जाने वाले रजिस्टरों में कॉपी किया जाता है और फिर कैली द्वारा स्थानीय चर में कॉपी किया जाता है। स्थानीय चर में मेमोरी स्थान है या नहीं, इस प्रकार केवल कैली की चिंता है।

एक प्रकार जहां दूसरी ओर एक कॉपी या मूव कंस्ट्रक्टर का उपयोग किया जाना चाहिए, वह इस तरह से कॉपी ऑपरेशन को विभाजित नहीं कर सकता है, इसलिए इसे स्मृति में पारित किया जाना चाहिए।

क्या C ++ मानकों की समिति ने हाल के वर्षों में इस बिंदु पर चर्चा की है, या कभी?

अगर मानकों निकायों ने इस पर विचार किया है तो मुझे कोई अंदाजा नहीं है।

मेरे लिए स्पष्ट समाधान यह है कि लैंगगॉव के लिए उचित विनाशकारी चालें (वर्तमान के "आधे या अधिक अनिर्दिष्ट राज्य" के मौजूदा आधे-तरफ़ा घर की बजाय), फिर "ट्रिविक डिस्ट्रक्टिव चाल" के लिए अनुमति के रूप में एक प्रकार का झंडा लगाने का एक तरीका पेश करें। "भले ही यह तुच्छ प्रतियों के लिए अनुमति नहीं देता है।

लेकिन इस तरह के समाधान के लिए WOULD को मौजूदा प्रकारों के लिए लागू करने के लिए मौजूदा कोड के ABI को तोड़ने की आवश्यकता होती है, जो काफी हद तक प्रतिरोध कर सकता है (हालांकि ABI नए C ++ मानक संस्करणों के परिणामस्वरूप टूटता नहीं है, उदाहरण के लिए, std :: string परिवर्तन C ++ 11 में एक ABI ब्रेक के परिणामस्वरूप ।।





abi