c++ - अप्रयुक्त स्ट्रिंग के संकलक अनुकूलन के असंगत व्यवहार




gcc compilation (2)

मुझे आश्चर्य हुआ कि संकलक ने एक std::string constructor / destructor pair के माध्यम से देखा जब तक मैंने आपका दूसरा उदाहरण नहीं देखा। यह नहीं था आप यहाँ जो देख रहे हैं, वह छोटे स्ट्रिंग अनुकूलन और इसके आसपास के कंपाइलर से संबंधित अनुकूलन है।

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

कंपाइलर वास्तव में अनावश्यक आवंटन और डील-डौल को समाप्त करने में खराब होते हैं, उन्हें लगभग उसी तरह से व्यवहार किया जाता है जैसे कि साइड इफेक्ट्स होते हैं और इस तरह से समाप्त होना असंभव है। जब आप छोटे स्ट्रिंग अनुकूलन सीमा पर जाते हैं, तो गतिशील आवंटन होते हैं और परिणाम वही होता है जो आप देखते हैं।

उदाहरण के तौर पे

void foo() {
    delete new int;
}

सबसे सरल, विनम्र आबंटन / निपटारा जोड़ी संभव है, फिर भी इस विधानसभा को O3 के तहत भी निकाला जाता है

sub     rsp, 8
mov     edi, 4
call    operator new(unsigned long)
mov     esi, 4
add     rsp, 8
mov     rdi, rax
jmp     operator delete(void*, unsigned long)

मैं उत्सुक हूँ कि निम्नलिखित कोड क्यों:

#include <string>
int main()
{
    std::string a = "ABCDEFGHIJKLMNO";
}

जब -O3 संकलित किया जाता है तो निम्न कोड प्राप्त होता है:

main:                                   # @main
    xor     eax, eax
    ret

(मैं पूरी तरह से समझता हूं कि अप्रयुक्त की कोई आवश्यकता नहीं है, इसलिए संकलक इसे उत्पन्न कोड से पूरी तरह से छोड़ सकता है)

हालांकि निम्नलिखित कार्यक्रम:

#include <string>
int main()
{
    std::string a = "ABCDEFGHIJKLMNOP"; // <-- !!! One Extra P 
}

पैदावार:

main:                                   # @main
        push    rbx
        sub     rsp, 48
        lea     rbx, [rsp + 32]
        mov     qword ptr [rsp + 16], rbx
        mov     qword ptr [rsp + 8], 16
        lea     rdi, [rsp + 16]
        lea     rsi, [rsp + 8]
        xor     edx, edx
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long)
        mov     qword ptr [rsp + 16], rax
        mov     rcx, qword ptr [rsp + 8]
        mov     qword ptr [rsp + 32], rcx
        movups  xmm0, xmmword ptr [rip + .L.str]
        movups  xmmword ptr [rax], xmm0
        mov     qword ptr [rsp + 24], rcx
        mov     rax, qword ptr [rsp + 16]
        mov     byte ptr [rax + rcx], 0
        mov     rdi, qword ptr [rsp + 16]
        cmp     rdi, rbx
        je      .LBB0_3
        call    operator delete(void*)
.LBB0_3:
        xor     eax, eax
        add     rsp, 48
        pop     rbx
        ret
        mov     rdi, rax
        call    _Unwind_Resume
.L.str:
        .asciz  "ABCDEFGHIJKLMNOP"

जब एक ही -O3 साथ संकलित किया गया। मुझे समझ में नहीं आता है कि यह क्यों नहीं पहचानता है कि a अभी भी अप्रयुक्त है, भले ही स्ट्रिंग एक बाइट लंबी हो।

यह प्रश्न gcc 9.1 और clang 8.0 के लिए प्रासंगिक है, (ऑनलाइन: https://gcc.godbolt.org/z/p1Z8Ns ) क्योंकि मेरे अवलोकन में अन्य संकलक पूरी तरह से अप्रयुक्त चर (ellcc) को छोड़ देते हैं या इसकी परवाह किए बिना कोड उत्पन्न करते हैं। स्ट्रिंग की लंबाई।


यह छोटे स्ट्रिंग अनुकूलन के कारण है। जब स्ट्रिंग डेटा अशक्त टर्मिनेटर सहित 16 वर्णों से कम या बराबर होता है, तो इसे बफर लोकल में std::string ऑब्जेक्ट में ही स्टोर किया जाता है। अन्यथा, यह ढेर पर मेमोरी आवंटित करता है और वहां पर डेटा संग्रहीत करता है।

पहला स्ट्रिंग "ABCDEFGHIJKLMNO" प्लस शून्य टर्मिनेटर का आकार 16 आकार का है। "P" जोड़ने से यह बफर से अधिक हो जाता है, इसलिए new को आंतरिक रूप से कहा जा रहा है, अनिवार्य रूप से एक सिस्टम कॉल के लिए अग्रणी है। कंपाइलर कुछ दूर अनुकूलन कर सकता है यदि यह सुनिश्चित करना संभव है कि कोई दुष्प्रभाव न हो। एक सिस्टम कॉल संभवतः ऐसा करना असंभव बनाता है - कसना द्वारा, निर्माण के तहत वस्तु के लिए एक बफर स्थानीय को बदलना इस तरह के साइड इफेक्ट विश्लेषण के लिए अनुमति देता है।

Libstdc ++, संस्करण 9.1 में स्थानीय बफर का पता लगाने, bits/basic_string.h इन हिस्सों bits/basic_string.h का पता चलता है:

template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
   // ...

  enum { _S_local_capacity = 15 / sizeof(_CharT) };

  union
    {
      _CharT           _M_local_buf[_S_local_capacity + 1];
      size_type        _M_allocated_capacity;
    };
   // ...
 };

जो आपको स्थानीय बफर आकार _S_local_capacity और स्वयं स्थानीय बफर ( _M_local_buf ) की सुविधा देता है। जब कंस्ट्रक्टर basic_string::_M_construct ट्रिगर करता है basic_string::_M_construct कहा जा रहा है, तो आपके पास bits/basic_string.tcc :

void _M_construct(_InIterator __beg, _InIterator __end, ...)
{
  size_type __len = 0;
  size_type __capacity = size_type(_S_local_capacity);

  while (__beg != __end && __len < __capacity)
  {
    _M_data()[__len++] = *__beg;
    ++__beg;
  }

जहां स्थानीय बफर अपनी सामग्री से भरा होता है। इस भाग के ठीक बाद, हम उस शाखा में पहुँच जाते हैं जहाँ स्थानीय क्षमता समाप्त हो जाती है - नया भंडारण आवंटित किया जाता है ( M_create में आवंटित के माध्यम से), स्थानीय बफर को नए भंडारण में कॉपी किया जाता है और शेष प्रारंभिक तर्क से भरा जाता है:

  while (__beg != __end)
  {
    if (__len == __capacity)
      {
        // Allocate more space.
        __capacity = __len + 1;
        pointer __another = _M_create(__capacity, __len);
        this->_S_copy(__another, _M_data(), __len);
        _M_dispose();
        _M_data(__another);
        _M_capacity(__capacity);
      }
    _M_data()[__len++] = *__beg;
    ++__beg;
  }

एक साइड नोट के रूप में, छोटे स्ट्रिंग अनुकूलन अपने आप में एक विषय है। अलग-अलग बिट्स को बड़े पैमाने पर कैसे अलग किया जा सकता है, इसके लिए एक भावना प्राप्त करने के लिए, मैं इस बात की सिफारिश करूंगा। इसमें यह भी उल्लेख किया गया है कि कैसे std::string कार्यान्वयन जो कि gcc (libstdc ++) के साथ काम करता है और मानक के नए संस्करणों से मेल खाने के लिए अतीत के दौरान बदल जाता है।






compiler-optimization