इस सूचक का उपयोग करने से गर्म लूप में अजीब विकृति का कारण बनता है


122

मैं हाल ही में एक अजीब deoptimization (या बल्कि अनुकूलन अवसर याद किया) आया था।

8-बिट पूर्णांक के लिए 3-बिट पूर्णांक के सरणियों के कुशल अनपैकिंग के लिए इस फ़ंक्शन पर विचार करें। यह प्रत्येक लूप पुनरावृत्ति में 16 इनट्स को अनपैक करता है:

void unpack3bit(uint8_t* target, char* source, int size) {
   while(size > 0){
      uint64_t t = *reinterpret_cast<uint64_t*>(source);
      target[0] = t & 0x7;
      target[1] = (t >> 3) & 0x7;
      target[2] = (t >> 6) & 0x7;
      target[3] = (t >> 9) & 0x7;
      target[4] = (t >> 12) & 0x7;
      target[5] = (t >> 15) & 0x7;
      target[6] = (t >> 18) & 0x7;
      target[7] = (t >> 21) & 0x7;
      target[8] = (t >> 24) & 0x7;
      target[9] = (t >> 27) & 0x7;
      target[10] = (t >> 30) & 0x7;
      target[11] = (t >> 33) & 0x7;
      target[12] = (t >> 36) & 0x7;
      target[13] = (t >> 39) & 0x7;
      target[14] = (t >> 42) & 0x7;
      target[15] = (t >> 45) & 0x7;
      source+=6;
      size-=6;
      target+=16;
   }
}

यहाँ कोड के कुछ हिस्सों के लिए उत्पन्न विधानसभा है:

 ...
 367:   48 89 c1                mov    rcx,rax
 36a:   48 c1 e9 09             shr    rcx,0x9
 36e:   83 e1 07                and    ecx,0x7
 371:   48 89 4f 18             mov    QWORD PTR [rdi+0x18],rcx
 375:   48 89 c1                mov    rcx,rax
 378:   48 c1 e9 0c             shr    rcx,0xc
 37c:   83 e1 07                and    ecx,0x7
 37f:   48 89 4f 20             mov    QWORD PTR [rdi+0x20],rcx
 383:   48 89 c1                mov    rcx,rax
 386:   48 c1 e9 0f             shr    rcx,0xf
 38a:   83 e1 07                and    ecx,0x7
 38d:   48 89 4f 28             mov    QWORD PTR [rdi+0x28],rcx
 391:   48 89 c1                mov    rcx,rax
 394:   48 c1 e9 12             shr    rcx,0x12
 398:   83 e1 07                and    ecx,0x7
 39b:   48 89 4f 30             mov    QWORD PTR [rdi+0x30],rcx
 ...

यह काफी प्रभावोत्पादक लगता है। बस एक के shift rightबाद एक andऔर फिर बफर के storeलिए एक target। लेकिन अब, जब मैं किसी संरचना में फ़ंक्शन को विधि में बदलता हूं तो क्या होता है:

struct T{
   uint8_t* target;
   char* source;
   void unpack3bit( int size);
};

void T::unpack3bit(int size) {
        while(size > 0){
           uint64_t t = *reinterpret_cast<uint64_t*>(source);
           target[0] = t & 0x7;
           target[1] = (t >> 3) & 0x7;
           target[2] = (t >> 6) & 0x7;
           target[3] = (t >> 9) & 0x7;
           target[4] = (t >> 12) & 0x7;
           target[5] = (t >> 15) & 0x7;
           target[6] = (t >> 18) & 0x7;
           target[7] = (t >> 21) & 0x7;
           target[8] = (t >> 24) & 0x7;
           target[9] = (t >> 27) & 0x7;
           target[10] = (t >> 30) & 0x7;
           target[11] = (t >> 33) & 0x7;
           target[12] = (t >> 36) & 0x7;
           target[13] = (t >> 39) & 0x7;
           target[14] = (t >> 42) & 0x7;
           target[15] = (t >> 45) & 0x7;
           source+=6;
           size-=6;
           target+=16;
        }
}

मुझे लगा कि उत्पन्न विधानसभा काफी समान होनी चाहिए, लेकिन ऐसा नहीं है। यहाँ इसका एक हिस्सा है:

...
 2b3:   48 c1 e9 15             shr    rcx,0x15
 2b7:   83 e1 07                and    ecx,0x7
 2ba:   88 4a 07                mov    BYTE PTR [rdx+0x7],cl
 2bd:   48 89 c1                mov    rcx,rax
 2c0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 2c3:   48 c1 e9 18             shr    rcx,0x18
 2c7:   83 e1 07                and    ecx,0x7
 2ca:   88 4a 08                mov    BYTE PTR [rdx+0x8],cl
 2cd:   48 89 c1                mov    rcx,rax
 2d0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 2d3:   48 c1 e9 1b             shr    rcx,0x1b
 2d7:   83 e1 07                and    ecx,0x7
 2da:   88 4a 09                mov    BYTE PTR [rdx+0x9],cl
 2dd:   48 89 c1                mov    rcx,rax
 2e0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 2e3:   48 c1 e9 1e             shr    rcx,0x1e
 2e7:   83 e1 07                and    ecx,0x7
 2ea:   88 4a 0a                mov    BYTE PTR [rdx+0xa],cl
 2ed:   48 89 c1                mov    rcx,rax
 2f0:   48 8b 17                mov    rdx,QWORD PTR [rdi] // Load, BAD!
 ...

जैसा कि आप देखते हैं, हमने loadप्रत्येक शिफ्ट ( mov rdx,QWORD PTR [rdi]) से पहले मेमोरी से एक अतिरिक्त अतिरेक पेश किया । यह targetसूचक की तरह लगता है (जो अब स्थानीय चर के बजाय एक सदस्य है) को इसमें संग्रहीत करने से पहले हमेशा लोड करना पड़ता है। यह कोड को काफी धीमा कर देता है (मेरे माप में लगभग 15%)।

पहले मुझे लगा कि शायद C ++ मेमोरी मॉडल लागू करता है कि एक सदस्य पॉइंटर को एक रजिस्टर में संग्रहीत नहीं किया जा सकता है, लेकिन फिर से लोड किया जाना है, लेकिन यह एक अजीब पसंद की तरह लग रहा था, क्योंकि यह बहुत अधिक व्यवहार्य अनुकूलन असंभव बना देगा। इसलिए मुझे बहुत आश्चर्य हुआ कि कंपाइलर ने targetयहां एक रजिस्टर में स्टोर नहीं किया ।

मैंने सदस्य सूचक को खुद को स्थानीय चर में कैशिंग करने की कोशिश की:

void T::unpack3bit(int size) {
    while(size > 0){
       uint64_t t = *reinterpret_cast<uint64_t*>(source);
       uint8_t* target = this->target; // << ptr cached in local variable
       target[0] = t & 0x7;
       target[1] = (t >> 3) & 0x7;
       target[2] = (t >> 6) & 0x7;
       target[3] = (t >> 9) & 0x7;
       target[4] = (t >> 12) & 0x7;
       target[5] = (t >> 15) & 0x7;
       target[6] = (t >> 18) & 0x7;
       target[7] = (t >> 21) & 0x7;
       target[8] = (t >> 24) & 0x7;
       target[9] = (t >> 27) & 0x7;
       target[10] = (t >> 30) & 0x7;
       target[11] = (t >> 33) & 0x7;
       target[12] = (t >> 36) & 0x7;
       target[13] = (t >> 39) & 0x7;
       target[14] = (t >> 42) & 0x7;
       target[15] = (t >> 45) & 0x7;
       source+=6;
       size-=6;
       this->target+=16;
    }
}

यह कोड अतिरिक्त स्टोर के बिना "अच्छा" कोडांतरक भी देता है। इसलिए मेरा अनुमान है: कंपाइलर को किसी संरचना के सदस्य पॉइंटर के भार को फहराने की अनुमति नहीं है, इसलिए इस तरह के "हॉट पॉइंटर" को हमेशा स्थानीय चर में संग्रहित किया जाना चाहिए।

  • तो, कंपाइलर इन भारों का अनुकूलन करने में असमर्थ क्यों है?
  • क्या यह C ++ मेमोरी मॉडल है जो इसे मना करता है? या यह बस मेरे संकलक की कमी है?
  • क्या मेरा अनुमान सही है या क्या सही कारण है कि अनुकूलन क्यों नहीं किया जा सकता है?

उपयोग में संकलक अनुकूलन के g++ 4.8.2-19ubuntu1साथ था -O3। मैंने भी clang++ 3.4-1ubuntu3इसी तरह के परिणामों के साथ प्रयास किया: क्लैंग स्थानीय targetसंकेतक के साथ विधि को वेक्टर करने में भी सक्षम है । हालाँकि, this->targetपॉइंटर का उपयोग करने से समान परिणाम प्राप्त होता है: प्रत्येक स्टोर से पहले पॉइंटर का अतिरिक्त भार।

मैंने कुछ समान तरीकों के कोडांतरक की जांच की और परिणाम समान है: ऐसा लगता है कि thisहमेशा एक सदस्य को स्टोर से पहले फिर से लोड करना पड़ता है, भले ही ऐसा लोड केवल लूप के बाहर फहराया जा सके। मुझे इन अतिरिक्त स्टोर से छुटकारा पाने के लिए बहुत सारे कोड को फिर से लिखना होगा, मुख्य रूप से पॉइंटर को खुद को एक स्थानीय चर में कैशिंग करके जो कि हॉट कोड के ऊपर घोषित किया गया है। लेकिन मैंने हमेशा इस तरह के विवरणों के बारे में सोचा था कि एक स्थानीय चर में एक सूचक को कैशिंग करने से निश्चित रूप से इन दिनों में समय से पहले अनुकूलन के लिए अर्हता प्राप्त होगी जहां कंपाइलरों ने इतनी चतुरता प्राप्त की है। लेकिन ऐसा लगता है कि मैं यहां गलत हूं । हॉट लूप में एक सदस्य पॉइंटर को कैशिंग करना एक आवश्यक मैनुअल अनुकूलन तकनीक लगती है।


5
यह सुनिश्चित नहीं है कि इसे नीचे-वोट क्यों मिला - यह एक दिलचस्प सवाल है। एफडब्ल्यूआईडब्ल्यू मैंने गैर-पॉइंटर सदस्य चर के साथ समान अनुकूलन समस्याओं को देखा है जहां समाधान समान है, अर्थात विधि के जीवनकाल के लिए स्थानीय चर में सदस्य चर को कैश करें। मैं अनुमान लगा रहा हूँ कि यह अलियासिंग नियमों के साथ कुछ करना है?
पॉल आर

1
ऐसा लगता है कि संकलक अनुकूलन नहीं करता है क्योंकि वह यह सुनिश्चित नहीं कर सकता है कि सदस्य कुछ "बाहरी" कोड के माध्यम से एक्सेस नहीं किया गया है। इसलिए यदि सदस्य को बाहर संशोधित किया जा सकता है, तो इसे हर बार एक्सेस किए जाने पर पुनः लोड किया जाना चाहिए। एक तरह की अस्थिरता की तरह माना जाता है ...
जीन-बैप्टिस्ट यूंसे

कोई उपयोग नहीं कर रहा this->है बस कृत्रिम चीनी है। समस्या चर (स्थानीय बनाम सदस्य) की प्रकृति और संकलक द्वारा इस तथ्य से संबंधित चीजों से संबंधित है।
जीन-बैप्टिस्ट युनुस

सूचक उपनाम के साथ कुछ भी करने के लिए?
यवेस डाएट

3
अधिक अर्थपूर्ण बात के रूप में, "समय से पहले अनुकूलन" केवल अनुकूलन के लिए लागू होता है, जो कि समय से पहले ही है, अर्थात्, प्रोफाइलिंग से पहले यह एक मुद्दा पाया गया है। इस मामले में, आपने परिश्रमपूर्वक विघटित और विघटित किया और एक मुद्दे के स्रोत को तैयार किया और एक समाधान तैयार किया। यह उस समाधान को लागू करने के लिए "समय से पहले" नहीं है।
raptortech97

जवाबों:


107

सूचक अलियासिंग समस्या लगती है, विडंबना यह है कि thisऔर this->target। कंपाइलर आपके द्वारा प्रारंभ की गई अश्लील संभावना को ध्यान में रख रहा है:

this->target = &this

उस स्थिति में, लेखन (और इस प्रकार ) this->target[0]की सामग्री को बदल देगा ।thisthis->target

मेमोरी एलियासिंग समस्या ऊपर तक सीमित नहीं है। सिद्धांत रूप में, this->target[XX]दिए गए () में दिए गए उचित मूल्य का कोई भी उपयोग XXइंगित कर सकता है this

मैं C से बेहतर वाकिफ हूं, जहां __restrict__कीवर्ड के साथ पॉइंटर वैरिएबल घोषित करके इसे रीमेड किया जा सकता है ।


18
मैं इसकी पुष्टि कर सकता हूं! बदलने targetसे uint8_tकरने के लिए uint16_t(ताकि सख्त अलियासिंग नियमों में लात) यह बदल दिया है। के साथ uint16_t, लोड हमेशा अनुकूलित होता है।
gexicide

1
प्रासंगिक: stackoverflow.com/questions/16138237/…
user541686

3
सामग्री को बदलना वह है thisजिसका आप मतलब नहीं है (यह एक चर नहीं है); आप की सामग्री को बदलने का मतलब है *this
मार्क वैन लीउवेन

@gexicide दिमाग में कैसे सख्त उर्फ ​​kicks और मुद्दे को ठीक करता है?
एचसीएसएफ

33

सख्त अलियासिंग नियम char*किसी भी अन्य सूचक को उर्फ ​​करने की अनुमति देता है। तो this->targetमई उर्फ साथ this, और अपने कोड विधि में, कोड के पहले भाग,

target[0] = t & 0x7;
target[1] = (t >> 3) & 0x7;
target[2] = (t >> 6) & 0x7;

वास्तव में है

this->target[0] = t & 0x7;
this->target[1] = (t >> 3) & 0x7;
this->target[2] = (t >> 6) & 0x7;

के रूप में thisजब आप संशोधित संशोधित किया जा सकता this->targetसामग्री।

एक बार this->targetस्थानीय चर में संचित होने के बाद , उपनाम स्थानीय चर के साथ संभव नहीं है।


1
तो, क्या हम एक सामान्य नियम के रूप में कह सकते हैं: जब भी आपके पास char*या void*आपकी संरचना में है, तो इसे लिखने से पहले स्थानीय चर में कैश करना सुनिश्चित करें?
gexicide

5
वास्तव में यह तब होता है जब आप char*सदस्य के रूप में आवश्यक नहीं, का उपयोग करते हैं ।
Jarod42

24

यहाँ मुद्दा सख्त अलियासिंग है जो कहता है कि हमें एक चार * के माध्यम से उपनाम देने की अनुमति है और इसलिए आपके मामले में संकलक अनुकूलन को रोकता है। हमें एक अलग प्रकार के सूचक के माध्यम से उपनाम करने की अनुमति नहीं है जो अपरिभाषित व्यवहार होगा, आम तौर पर एसओ पर हम इस समस्या को देखते हैं जो उपयोगकर्ता असंगत सूचक प्रकारों के माध्यम से उपनाम करने का प्रयास कर रहे हैं

यह अहस्ताक्षरित चार के रूप में uint8_t को लागू करने के लिए उचित प्रतीत होगा और अगर हम कोलिरु पर cstdint को देखते हैं, तो इसमें stdint.h शामिल है, जो uint8_t टाइप करता है:

typedef unsigned char       uint8_t;

यदि आपने एक और गैर-चार प्रकार का उपयोग किया है, तो संकलक को अनुकूलित करने में सक्षम होना चाहिए।

यह C ++ मानक अनुभाग के 3.10 मसौदे और नियम में शामिल है, जो कहता है:

यदि कोई प्रोग्राम किसी वस्तु के संग्रहित मूल्य को निम्न प्रकार के व्यवहार के अलावा किसी अन्य वस्तु के ग्लव्यू के माध्यम से एक्सेस करने का प्रयास करता है तो व्यवहार अपरिभाषित है।

और निम्नलिखित बुलेट शामिल हैं:

  • एक चार या अहस्ताक्षरित चार प्रकार।

ध्यान दें, मैंने एक प्रश्न में संभावित कार्य के बारे में एक टिप्पणी पोस्ट की है जिसमें पूछा गया है कि uint8_t igned अहस्ताक्षरित चार कब है? और सिफारिश थी:

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

चूँकि C ++ उस प्रतिबंधित कीवर्ड का समर्थन नहीं करता है जिसे आपको कंपाइलर एक्सटेंशन पर निर्भर करना पड़ता है, उदाहरण के लिए gcc __restrict__ का उपयोग करता है इसलिए यह पूरी तरह से पोर्टेबल नहीं है लेकिन दूसरा सुझाव होना चाहिए।


यह एक ऐसी जगह का एक उदाहरण है जहां मानक ऑप्टिमाइज़र के लिए बदतर है, एक नियम से एक संकलक मान सकता है कि टाइप टी के किसी ऑब्जेक्ट तक दो पहुंच के बीच, या ऐसी पहुंच और लूप / फ़ंक्शन का प्रारंभ या अंत। जिसमें ऐसा होता है, स्टोरेज तक सभी एक्सेस एक ही ऑब्जेक्ट का उपयोग करेंगे जब तक कि कोई हस्तक्षेप करने वाला ऑपरेशन उस ऑब्जेक्ट (या इसके लिए एक पॉइंटर / रेफरेंस) का इस्तेमाल किसी पॉइंटर या रेफरेंस को किसी अन्य ऑब्जेक्ट को प्राप्त करने के लिए नहीं करता है । ऐसा नियम "चरित्र-प्रकार के अपवाद" की आवश्यकता को समाप्त कर देगा, जो कोड के प्रदर्शन को मार सकता है जो बाइट्स के अनुक्रम के साथ काम करता है।
सुपरकैट
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.