আমি সম্প্রতি একটি অদ্ভুত ডিওপিমাইজেশন পেয়েছি (বা বরং অপটিমাইজেশনের সুযোগ মিস)।
8-বিট পূর্ণসংখ্যার 3-বিট পূর্ণসংখ্যার অ্যারেগুলিকে দক্ষভাবে আনপ্যাক করার জন্য এই ফাংশনটি বিবেচনা করুন। এটি প্রতিটি লুপের পুনরাবৃত্তিতে 16 টি ints প্যাক করে:
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%)।
প্রথমে আমি ভেবেছিলাম সম্ভবত সি ++ মেমরি মডেল প্রয়োগ করে যে কোনও সদস্য পয়েন্টার কোনও রেজিস্টারে সংরক্ষণ করা হতে পারে না তবে তাকে পুনরায় লোড করতে হবে, তবে এটি একটি বিশ্রী পছন্দ বলে মনে হয়েছিল, কারণ এটি প্রচুর কার্যকর অপটিমাইজেশনকে অসম্ভব করে তুলবে। তাই আমি খুব অবাক হয়েছিলাম যে সংকলকটি 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;
}
}
এই কোডটি অতিরিক্ত স্টোর ছাড়াই "ভাল" এসেমব্লার লাভ করে। সুতরাং আমার অনুমানটি হ'ল: সংকলকটিকে কোনও কাঠামোর সদস্য পয়েন্টারের লোড উত্তোলনের অনুমতি নেই, সুতরাং এই জাতীয় "হট পয়েন্টার" সর্বদা স্থানীয় ভেরিয়েবলে সংরক্ষণ করা উচিত।
- সুতরাং, সংকলক কেন এই লোডগুলি অপ্টিমাইজ করতে অক্ষম?
- এটি কি সি ++ মেমরি মডেল যা এটিকে নিষিদ্ধ করে? বা এটি কেবল আমার সংকলকের একটি ঘাটতি?
- আমার অনুমানটি কি সঠিক বা সঠিক কারণটি অপ্টিমাইজেশন সম্পাদন করা যায় না?
ব্যবহৃত সংকলকটি অপ্টিমাইজেশনের g++ 4.8.2-19ubuntu1
সাথে ছিল -O3
। আমি clang++ 3.4-1ubuntu3
অনুরূপ ফলাফলের সাথে চেষ্টা করেছিলাম : ঝুঁকি এমনকি স্থানীয় target
পয়েন্টারটির সাহায্যে পদ্ধতিটিকে ভেক্টরাইজ করতে সক্ষম । তবে this->target
পয়েন্টার ব্যবহার করে একই ফলাফল পাওয়া যায়: প্রতিটি স্টোরের আগে পয়েন্টারের অতিরিক্ত লোড load
আমি কিছু অনুরূপ পদ্ধতির এসেম্বলারকে পরীক্ষা করেছিলাম এবং ফলাফলটি একই: এটি মনে হয় যে কোনও সদস্যকে this
সর্বদা একটি স্টোরের আগে পুনরায় লোড করতে হবে, এমনকি যদি এ জাতীয় লোডটি কেবল লুপের বাইরে উত্তোলন করা যায়। এই অতিরিক্ত স্টোরগুলি থেকে মুক্তি পাওয়ার জন্য আমাকে প্রচুর কোড পুনর্লিখন করতে হবে, প্রধানত পয়েন্টারটিকে স্থানীয় স্থানীয় ভেরিয়েবলে ক্যাশে করে যা হট কোডের উপরে ঘোষণা করা হয়। তবে আমি সবসময়ই ভেবেছিলাম যে স্থানীয় ভেরিয়েবলের মধ্যে কোনও পয়েন্টারকে ক্যাশে দেওয়ার মতো বিবরণ দিয়ে ফিডিং করা অবশ্যই এই সময়ের মধ্যে অকালীন অপ্টিমাইজেশনের যোগ্যতা অর্জন করবে যেখানে সংকলকরা এত চালাক হয়ে গেছে। তবে মনে হচ্ছে আমি এখানে ভুল । একটি গরম লুপে সদস্য পয়েন্টারকে ক্যাচ করা একটি প্রয়োজনীয় ম্যানুয়াল অপটিমাইজেশন কৌশল বলে মনে হচ্ছে।
this->
করা কেবল সিনট্যাকটিক চিনি। সমস্যাটি ভেরিয়েবলের (স্থানীয় বনাম সদস্য) প্রকৃতির এবং সংস্থাপকটি এই সত্যটি থেকে যে বিষয়গুলি হ্রাস করে তার সাথে সম্পর্কিত।