ตอบคำถาม Stack Overflow (อันนี้ ) ฉันพบปัญหาย่อยที่น่าสนใจ วิธีที่เร็วที่สุดในการจัดเรียงอาร์เรย์ของ 6 จำนวนเต็มคืออะไร?
เนื่องจากคำถามอยู่ในระดับต่ำมาก:
- เราไม่สามารถสมมติว่ามีไลบรารี (และการโทรเองมีค่าใช้จ่าย) เพียงธรรมดา C
- เพื่อหลีกเลี่ยงการล้างท่อส่งคำสั่ง (ที่มีค่าใช้จ่ายสูงมาก ) เราควรจะลดกิ่งกระโดดและการควบคุมการไหลอื่น ๆ ทุกชนิด (เช่นที่ซ่อนอยู่หลังจุดลำดับใน
&&
หรือ||
) - ห้องมีข้อ จำกัด และการลดการลงทะเบียนและการใช้หน่วยความจำก็เป็นปัญหา
คำถามนี้เป็นคำถามประเภทกอล์ฟที่เป้าหมายไม่ได้ลดความยาวของแหล่งที่มา แต่ลดระยะเวลาดำเนินการลง ผมเรียกรหัสมัน Zening 'ที่ใช้ในชื่อของหนังสือเล่มนี้เซนของการเพิ่มประสิทธิภาพรหัสโดยไมเคิล Abrashและต่อมา
ทำไมมันถึงน่าสนใจมีหลายเลเยอร์:
- ตัวอย่างนั้นง่ายและเข้าใจง่ายและวัดผลไม่เกี่ยวข้องกับทักษะ C มากนัก
- มันแสดงผลของการเลือกอัลกอริทึมที่ดีสำหรับปัญหา แต่ยังมีผลกระทบของคอมไพเลอร์และฮาร์ดแวร์พื้นฐาน
นี่คือการดำเนินการอ้างอิงของฉัน (ไร้เดียงสาไม่เหมาะ) และชุดทดสอบของฉัน
#include <stdio.h>
static __inline__ int sort6(int * d){
char j, i, imin;
int tmp;
for (j = 0 ; j < 5 ; j++){
imin = j;
for (i = j + 1; i < 6 ; i++){
if (d[i] < d[imin]){
imin = i;
}
}
tmp = d[j];
d[j] = d[imin];
d[imin] = tmp;
}
}
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
}
int main(int argc, char ** argv){
int i;
int d[6][5] = {
{1, 2, 3, 4, 5, 6},
{6, 5, 4, 3, 2, 1},
{100, 2, 300, 4, 500, 6},
{100, 2, 3, 4, 500, 6},
{1, 200, 3, 4, 5, 600},
{1, 1, 2, 1, 2, 1}
};
unsigned long long cycles = rdtsc();
for (i = 0; i < 6 ; i++){
sort6(d[i]);
/*
* printf("d%d : %d %d %d %d %d %d\n", i,
* d[i][0], d[i][6], d[i][7],
* d[i][8], d[i][9], d[i][10]);
*/
}
cycles = rdtsc() - cycles;
printf("Time is %d\n", (unsigned)cycles);
}
ผลดิบ
ขณะที่จำนวนของสายพันธุ์กลายเป็นขนาดใหญ่ฉันรวบรวมพวกเขาทั้งหมดในชุดทดสอบที่สามารถพบได้ที่นี่ การทดสอบจริงที่ใช้มีน้อยกว่าที่แสดงไว้ข้างต้นเล็กน้อยขอบคุณ Kevin Stock คุณสามารถรวบรวมและดำเนินการในสภาพแวดล้อมของคุณเอง ฉันค่อนข้างสนใจพฤติกรรมในสถาปัตยกรรม / คอมไพเลอร์เป้าหมายที่ต่างกัน (ตกลงเอาไว้ตอบแล้วฉันจะ +1 ผู้มีส่วนร่วมของชุดผลลัพธ์ใหม่)
ฉันให้คำตอบกับ Daniel Stutzbach (สำหรับการเล่นกอล์ฟ) เมื่อหนึ่งปีก่อนเพราะเขาอยู่ที่แหล่งที่มาของการแก้ปัญหาที่เร็วที่สุดในเวลานั้น (เครือข่ายคัดแยก)
Linux 64 bits, gcc 4.6.1 64 bits, Intel Core 2 Duo E8400, -O2
- โทรโดยตรงไปยังฟังก์ชั่นห้องสมุด qsort: 689.38
- การใช้งานแบบไร้เดียงสา (เรียงลำดับการแทรก): 285.70
- เรียงลำดับการแทรก (Daniel Stutzbach): 142.12
- แทรกการเรียงลำดับ Unrolled: 125.47
- อันดับสั่งซื้อ: 102.26
- อันดับสั่งซื้อด้วยการลงทะเบียน: 58.03
- เครือข่ายเรียงลำดับ (Daniel Stutzbach): 111.68
- เครือข่ายการเรียงลำดับ (Paul R): 66.36
- การเรียงลำดับเครือข่าย 12 ที่มีการสลับที่รวดเร็ว: 58.86
- เครือข่ายการเรียงลำดับ 12 Swap เรียงลำดับใหม่: 53.74
- เครือข่ายการเรียงลำดับ 12 Simple Swap ที่เรียงลำดับใหม่: 31.54
- จัดเรียงเครือข่ายการเรียงลำดับใหม่ที่มีการสลับที่รวดเร็ว: 31.54
- เรียงลำดับเครือข่ายใหม่ด้วยการสลับที่รวดเร็ว V2: 33.63
- Inline Bubble Sort (Paolo Bonzini): 48.85
- เรียงแทรกที่ไม่ได้ควบคุม (เปาโล Bonzini): 75.30
Linux 64 bits, gcc 4.6.1 64 bits, Intel Core 2 Duo E8400, -O1
- โทรโดยตรงไปยังฟังก์ชันไลบรารี qsort: 705.93
- การใช้งานแบบไร้เดียงสา (เรียงลำดับการแทรก): 135.60
- เรียงลำดับการแทรก (Daniel Stutzbach): 142.11
- แทรกการเรียงลำดับ Unrolled: 126.75
- อันดับสั่งซื้อ: 46.42
- อันดับสั่งซื้อด้วยการลงทะเบียน: 43.58
- เครือข่ายเรียงลำดับ (Daniel Stutzbach): 115.57
- เครือข่ายการเรียงลำดับ (Paul R): 64.44
- การเรียงลำดับเครือข่าย 12 ที่มีการสลับที่รวดเร็ว: 61.98
- เครือข่ายการเรียงลำดับ 12 Swap เรียงลำดับใหม่: 54.67
- เครือข่ายการเรียงลำดับ 12 Simple Swap ที่เรียงลำดับใหม่: 31.54
- จัดเรียงเครือข่ายการเรียงลำดับใหม่ที่มีการสลับที่รวดเร็ว: 31.24
- เรียงลำดับเครือข่ายใหม่ด้วยการสลับที่รวดเร็ว V2: 33.07
- Inline Bubble Sort (Paolo Bonzini): 45.79
- เรียงแทรกที่ไม่ได้ควบคุม (เปาโล Bonzini): 80.15
ฉันรวมทั้งผลลัพธ์ -O1 และ -O2 เพราะน่าแปลกใจสำหรับหลาย ๆ โปรแกรม O2 มีประสิทธิภาพน้อยกว่า O1 ฉันสงสัยว่าการเพิ่มประสิทธิภาพเฉพาะใดมีผลกระทบนี้
ความคิดเห็นเกี่ยวกับโซลูชันที่เสนอ
เรียงลำดับการแทรก (Daniel Stutzbach)
ตามที่คาดไว้การย่อขนาดสาขาจึงเป็นความคิดที่ดี
เครือข่ายคัดแยก (Daniel Stutzbach)
ดีกว่าการเรียงลำดับการแทรก ฉันสงสัยว่าเอฟเฟกต์หลักไม่ได้มาจากการหลีกเลี่ยงลูปภายนอกหรือไม่ ฉันลองใช้การเรียงลำดับการแทรกที่ไม่ได้ลงทะเบียนเพื่อตรวจสอบและแน่นอนว่าเราได้ตัวเลขที่เหมือนกัน (รหัสอยู่ที่นี่ )
เครือข่ายการเรียงลำดับ (Paul R)
ที่ดีที่สุดจนถึง ฉันรหัสจริงที่ใช้ในการทดสอบที่นี่ ยังไม่รู้ว่าทำไมมันถึงเร็วกว่าการใช้เครือข่ายการเรียงลำดับอื่นเกือบสองเท่า ผ่านพารามิเตอร์หรือไม่ เร็วสุดไหม
การเรียงลำดับเครือข่าย 12 SWAP พร้อมการสลับที่รวดเร็ว
ตามคำแนะนำของ Daniel Stutzbach ฉันได้รวมเครือข่ายการจัดเรียงสลับ 12 รายการของเขาเข้ากับการสลับเร็วแบบไม่มีสาขา (รหัสอยู่ที่นี่ ) มันเร็วกว่ามากที่สุดเท่าที่จะทำได้ด้วยอัตรากำไรขั้นต้นเล็กน้อย (ประมาณ 5%) ตามที่คาดไว้โดยใช้การแลกเปลี่ยนน้อย 1 ครั้ง
นอกจากนี้ยังเป็นที่น่าสนใจที่จะสังเกตเห็นว่าการแลกเปลี่ยนแบบไร้สาขาดูเหมือนจะมีประสิทธิภาพน้อยกว่าแบบง่าย ๆ ที่ใช้หากใช้กับสถาปัตยกรรม PPC
กำลังเรียก Library qsort
หากต้องการให้จุดอ้างอิงอื่นฉันได้ลองตามที่แนะนำให้เพียงเรียกไลบรารี qsort (รหัสอยู่ที่นี่ ) ตามที่คาดไว้มันช้ากว่ามาก: 10 ถึง 30 เท่าช้าลง ... เนื่องจากเห็นได้ชัดกับชุดทดสอบใหม่ปัญหาหลักน่าจะเป็นภาระเริ่มต้นของไลบรารีหลังจากการโทรครั้งแรกและเปรียบเทียบได้ไม่ดีนักเมื่อเทียบกับคนอื่น ๆ รุ่น มันช้าลงระหว่าง 3 และ 20 เท่าบน Linux ของฉัน ในบางสถาปัตยกรรมที่ใช้สำหรับการทดสอบโดยคนอื่น ๆ ดูเหมือนว่าจะเร็วขึ้น (ฉันประหลาดใจจริง ๆ โดยที่ไลบรารี qsort ใช้ API ที่ซับซ้อนมากขึ้น)
อันดับการสั่งซื้อ
เร็กซ์เคอร์เสนอวิธีอื่นที่แตกต่างกันโดยสิ้นเชิง: สำหรับแต่ละรายการของอาเรย์จะคำนวณตำแหน่งสุดท้าย สิ่งนี้มีประสิทธิภาพเพราะลำดับการคำนวณไม่ต้องการสาขา ข้อเสียของวิธีนี้คือจะต้องใช้จำนวนหน่วยความจำของอาเรย์สามเท่า (หนึ่งชุดของอาเรย์และตัวแปรเพื่อเก็บคำสั่งการจัดอันดับ) ผลการดำเนินงานที่น่าแปลกใจมาก (และน่าสนใจ) ในสถาปัตยกรรมอ้างอิงของฉันกับระบบปฏิบัติการ 32 บิตและ Intel Core2 Quad E8300 จำนวนรอบน้อยกว่า 1,000 เล็กน้อย (เช่นเครือข่ายการเรียงลำดับที่มีการแยกแบรนช์) แต่เมื่อรวบรวมและดำเนินการในกล่อง 64 บิตของฉัน (Intel Core2 Duo) มันทำงานได้ดีขึ้นมาก: มันกลายเป็นเร็วที่สุดจนถึงตอนนี้ ในที่สุดฉันก็พบเหตุผลที่แท้จริง กล่อง 32 บิตของฉันใช้ gcc 4.4.1 และกล่อง 64 บิตของฉัน gcc 4.4
อัปเดต :
เมื่อตัวเลขที่เผยแพร่ข้างต้นแสดงให้เห็นว่าเอฟเฟกต์นี้ยังคงได้รับการปรับปรุงโดย gcc รุ่นที่ใหม่กว่าและลำดับการจัดอันดับก็เร็วขึ้นอย่างต่อเนื่องเป็นสองเท่าของทางเลือกอื่น
การเรียงลำดับเครือข่าย 12 ที่มีการเรียงลำดับใหม่
ประสิทธิภาพที่น่าทึ่งของข้อเสนอ Rex Kerr กับ gcc 4.4.3 ทำให้ฉันสงสัยว่า: โปรแกรมที่มีการใช้หน่วยความจำ 3 ครั้งเร็วกว่าเครือข่ายการคัดแยกที่ไม่มีสาขาได้อย่างไร สมมติฐานของฉันคือว่ามันมีการพึ่งพาของการอ่านชนิดน้อยกว่าหลังจากการเขียนอนุญาตให้ใช้ตัวกำหนดตารางเวลาการสอน superscalar ของ x86 ได้ดีขึ้น นั่นทำให้ฉันมีความคิด: เรียงลำดับการแลกเปลี่ยนใหม่เพื่อลดการอ่านหลังจากการเขียนอ้างอิง พูดง่ายกว่า: เมื่อคุณSWAP(1, 2); SWAP(0, 2);
ต้องรอให้การแลกเปลี่ยนครั้งแรกเสร็จสิ้นก่อนที่จะทำการสลับครั้งที่สองเพราะทั้งคู่เข้าถึงเซลล์หน่วยความจำทั่วไป เมื่อคุณทำSWAP(1, 2); SWAP(4, 5);
โปรเซสเซอร์สามารถดำเนินการทั้งคู่ในแบบคู่ขนาน ฉันลองและใช้งานได้ตามที่คาดหวังเครือข่ายการเรียงลำดับจะทำงานเร็วขึ้นประมาณ 10%
การเรียงลำดับเครือข่าย 12 ด้วยการสลับอย่างง่าย
หนึ่งปีหลังจากโพสต์ดั้งเดิม Steinar H. Gunderson แนะนำว่าเราไม่ควรพยายามที่จะเอาชนะสติปัญญาของคอมไพเลอร์และทำให้รหัสการสลับง่าย เป็นความคิดที่ดีเพราะรหัสที่ได้นั้นเร็วกว่าประมาณ 40% นอกจากนี้เขายังเสนอการแลกเปลี่ยนที่ปรับให้เหมาะสมด้วยมือโดยใช้รหัสการประกอบแบบอินไลน์ x86 ที่ยังคงสามารถสำรองได้มากขึ้น สิ่งที่น่าประหลาดใจที่สุด (มันบอกว่าปริมาณของจิตวิทยาของโปรแกรมเมอร์) คือเมื่อหนึ่งปีที่แล้วไม่มีการทดลองใช้การแลกเปลี่ยนเวอร์ชันนั้น รหัสผมใช้ในการทดสอบที่นี่ คนอื่น ๆ แนะนำวิธีอื่นในการเขียน C แลกเปลี่ยนอย่างรวดเร็ว แต่ให้ผลการแสดงเช่นเดียวกับวิธีที่เรียบง่ายด้วยคอมไพเลอร์ที่ดี
รหัส "ดีที่สุด" ตอนนี้เป็นดังนี้:
static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x)
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
const int b = max(d[x], d[y]); \
d[x] = a; d[y] = b; }
SWAP(1, 2);
SWAP(4, 5);
SWAP(0, 2);
SWAP(3, 5);
SWAP(0, 1);
SWAP(3, 4);
SWAP(1, 4);
SWAP(0, 3);
SWAP(2, 5);
SWAP(1, 3);
SWAP(2, 4);
SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}
หากเราเชื่อว่าชุดทดสอบของเรา (และใช่มันค่อนข้างแย่มีประโยชน์เพียงสั้น ๆ ง่ายและเข้าใจสิ่งที่เราวัดได้) จำนวนรอบเฉลี่ยของโค้ดผลลัพธ์สำหรับการเรียงลำดับหนึ่งต่ำกว่า 40 รอบ ( ทำการทดสอบ 6 ครั้ง) นั่นทำให้การแลกเปลี่ยนแต่ละครั้งมีค่าเฉลี่ย 4 รอบ ฉันเรียกมันว่าเร็วอย่างน่าอัศจรรย์ การปรับปรุงอื่น ๆ ที่เป็นไปได้?
__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
เพราะ rdtsc ทำให้คำตอบใน EDX: EAX ในขณะที่ GCC คาดว่ามันจะอยู่ในการลงทะเบียน 64- บิตเดียว คุณสามารถดูข้อผิดพลาดโดยการรวบรวมที่ -O3 ดูความคิดเห็นด้านล่างของฉันที่มีต่อ Paul R เกี่ยวกับ SWAP ที่เร็วขึ้น
CMP EAX, EBX; SBB EAX, EAX
จะใส่ 0 หรือ 0xFFFFFFFF EAX
ขึ้นอยู่กับว่าEAX
มีขนาดใหญ่กว่าหรือเล็กกว่าEBX
ตามลำดับ SBB
คือ "ลบด้วยการยืม" ซึ่งเป็นคู่ของADC
("เพิ่มด้วยการพกพา"); บิตสถานะที่คุณอ้างถึงคือบิตพกพา จากนั้นอีกครั้งฉันจำได้ว่าADC
และSBB
มีความล่าช้าอย่างมากและปริมาณงานใน Pentium 4 เทียบกับADD
และSUB
และยังคงช้ากว่า CPU Core เป็นสองเท่า ตั้งแต่ 80386 ยังมีคำแนะนำแบบมีเงื่อนไขSETcc
และCMOVcc
การย้ายแบบมีเงื่อนไข แต่พวกมันก็ช้าเช่นกัน
x-y
และx+y
จะไม่ทำให้เกิดอันเดอร์โฟล์หรือโอเวอร์โฟลว์หรือไม่?