मैं हाल ही में एक अजीब 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
हमेशा एक सदस्य को स्टोर से पहले फिर से लोड करना पड़ता है, भले ही ऐसा लोड केवल लूप के बाहर फहराया जा सके। मुझे इन अतिरिक्त स्टोर से छुटकारा पाने के लिए बहुत सारे कोड को फिर से लिखना होगा, मुख्य रूप से पॉइंटर को खुद को एक स्थानीय चर में कैशिंग करके जो कि हॉट कोड के ऊपर घोषित किया गया है। लेकिन मैंने हमेशा इस तरह के विवरणों के बारे में सोचा था कि एक स्थानीय चर में एक सूचक को कैशिंग करने से निश्चित रूप से इन दिनों में समय से पहले अनुकूलन के लिए अर्हता प्राप्त होगी जहां कंपाइलरों ने इतनी चतुरता प्राप्त की है। लेकिन ऐसा लगता है कि मैं यहां गलत हूं । हॉट लूप में एक सदस्य पॉइंटर को कैशिंग करना एक आवश्यक मैनुअल अनुकूलन तकनीक लगती है।
this->
है बस कृत्रिम चीनी है। समस्या चर (स्थानीय बनाम सदस्य) की प्रकृति और संकलक द्वारा इस तथ्य से संबंधित चीजों से संबंधित है।