ฉันเพิ่งเจอการลดประสิทธิภาพแบบแปลก ๆ (หรือค่อนข้างพลาดโอกาสในการเพิ่มประสิทธิภาพ)
พิจารณาฟังก์ชันนี้เพื่อการคลายอาร์เรย์ของจำนวนเต็ม 3 บิตเป็นจำนวนเต็ม 8 บิตอย่างมีประสิทธิภาพ มันคลาย 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% ในการวัดของฉัน)
ก่อนอื่นฉันคิดว่าโมเดลหน่วยความจำ 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
จะต้องโหลดซ้ำก่อนร้านค้าเสมอแม้ว่าโหลดดังกล่าวจะสามารถยกออกนอกลูปได้ก็ตาม ฉันจะต้องเขียนโค้ดใหม่จำนวนมากเพื่อกำจัดร้านค้าเพิ่มเติมเหล่านี้ส่วนใหญ่โดยการแคชตัวชี้ตัวเองลงในตัวแปรท้องถิ่นที่ประกาศไว้เหนือรหัสร้อน แต่ฉันคิดเสมอว่าการเล่นซอกับรายละเอียดเช่นการแคชพอยน์เตอร์ในตัวแปรโลคัลจะมีสิทธิ์ได้รับการปรับให้เหมาะสมก่อนเวลาอันควรในสมัยนี้ที่คอมไพเลอร์ฉลาดมาก แต่ดูเหมือนว่าฉันผิดที่นี่ การแคชตัวชี้สมาชิกใน hot loop ดูเหมือนจะเป็นเทคนิคการเพิ่มประสิทธิภาพด้วยตนเองที่จำเป็น
this->
เป็นเพียงแค่น้ำตาลซินแทติก ปัญหาเกี่ยวข้องกับลักษณะของตัวแปร (local vs member) และสิ่งที่คอมไพเลอร์อนุมานจากข้อเท็จจริงนี้