ประสิทธิภาพสูงสุดในเชิงทฤษฎีของการดำเนินการจุดลอย 4 จุด (ความแม่นยำสองเท่า) ต่อรอบสามารถทำได้บนซีพียู x86-64 ที่ทันสมัยของ Intel?
เท่าที่ฉันเข้าใจมันใช้เวลาสามรอบสำหรับSSE add
และห้ารอบเพื่อmul
ให้เสร็จสมบูรณ์บน CPU Intel ส่วนใหญ่ที่ทันสมัย (ดูตัวอย่าง'Instruction Tables' ของ Agner Fog ) เนื่องจากการส่งไปป์ไลน์หนึ่งสามารถรับปริมาณงานหนึ่งadd
ต่อรอบหากอัลกอริทึมมีการสรุปอิสระอย่างน้อยสามครั้ง เนื่องจากเป็นจริงสำหรับแพ็กเกจaddpd
รวมถึงaddsd
รุ่นสเกลาร์และรีจิสเตอร์ SSE สามารถมีปริมาณงานสองdouble
รายการได้มากถึงสอง flops ต่อรอบ
นอกจากนี้ดูเหมือนว่า (แม้ว่าฉันจะไม่เห็นเอกสารที่ถูกต้องเกี่ยวกับเรื่องนี้) add
และmul
สามารถดำเนินการในแบบคู่ขนานโดยให้ทรูพุตสูงสุดตามทฤษฎีของสี่ flops ต่อรอบ
อย่างไรก็ตามฉันไม่สามารถจำลองประสิทธิภาพนั้นด้วยโปรแกรม C / C ++ อย่างง่าย ความพยายามที่ดีที่สุดของฉันส่งผลให้ประมาณ 2.7 flops / รอบ หากใครสามารถมีส่วนร่วม C / C ++ หรือโปรแกรมแอสเซมเบลอร์ที่แสดงให้เห็นถึงประสิทธิภาพสูงสุดที่จะได้รับการชื่นชมอย่างมาก
ความพยายามของฉัน:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>
double stoptime(void) {
struct timeval t;
gettimeofday(&t,NULL);
return (double) t.tv_sec + t.tv_usec/1000000.0;
}
double addmul(double add, double mul, int ops){
// Need to initialise differently otherwise compiler might optimise away
double sum1=0.1, sum2=-0.1, sum3=0.2, sum4=-0.2, sum5=0.0;
double mul1=1.0, mul2= 1.1, mul3=1.2, mul4= 1.3, mul5=1.4;
int loops=ops/10; // We have 10 floating point operations inside the loop
double expected = 5.0*add*loops + (sum1+sum2+sum3+sum4+sum5)
+ pow(mul,loops)*(mul1+mul2+mul3+mul4+mul5);
for (int i=0; i<loops; i++) {
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
}
return sum1+sum2+sum3+sum4+sum5+mul1+mul2+mul3+mul4+mul5 - expected;
}
int main(int argc, char** argv) {
if (argc != 2) {
printf("usage: %s <num>\n", argv[0]);
printf("number of operations: <num> millions\n");
exit(EXIT_FAILURE);
}
int n = atoi(argv[1]) * 1000000;
if (n<=0)
n=1000;
double x = M_PI;
double y = 1.0 + 1e-8;
double t = stoptime();
x = addmul(x, y, n);
t = stoptime() - t;
printf("addmul:\t %.3f s, %.3f Gflops, res=%f\n", t, (double)n/t/1e9, x);
return EXIT_SUCCESS;
}
รวบรวมด้วย
g++ -O2 -march=native addmul.cpp ; ./a.out 1000
สร้างเอาต์พุตต่อไปนี้บน Intel Core i5-750, 2.66 GHz
addmul: 0.270 s, 3.707 Gflops, res=1.326463
นั่นคือประมาณ 1.4 flops ต่อรอบ ดูโค้ดแอสเซมเบลอร์ที่มี
g++ -S -O2 -march=native -masm=intel addmul.cpp
ลูปหลักดูเหมือนจะเหมาะสมที่สุดสำหรับฉัน:
.L4:
inc eax
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
mulsd xmm5, xmm3
mulsd xmm1, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
addsd xmm10, xmm2
addsd xmm9, xmm2
cmp eax, ebx
jne .L4
การเปลี่ยนรุ่นสเกลาร์ด้วยรุ่นที่บรรจุ ( addpd
และmulpd
) จะเพิ่มจำนวน flop เป็นสองเท่าโดยไม่เปลี่ยนเวลาดำเนินการและดังนั้นฉันจะได้รับเพียง 2.8 flop ต่อรอบ มีตัวอย่างง่ายๆที่ประสบความสำเร็จสี่ flops ต่อรอบ?
โปรแกรมเล็ก ๆ ที่ดีโดย Mysticial; นี่คือผลลัพธ์ของฉัน (เรียกใช้เพียงไม่กี่วินาทีแม้ว่า):
gcc -O2 -march=nocona
: 5.6 Gflops จาก 10.66 Gflops (2.1 flops / รอบ)cl /O2
, openmp ถูกนำออก: 10.1 Gflops จาก 10.66 Gflops (3.8 flops / รอบ)
ดูเหมือนว่าจะซับซ้อนเล็กน้อย แต่ข้อสรุปของฉัน:
gcc -O2
เปลี่ยนลำดับของการดำเนินการจุดลอยตัวอิสระโดยมีจุดประสงค์ในการสลับaddpd
และmulpd
ถ้าเป็นไปได้gcc-4.6.2 -O2 -march=core2
เช่นเดียวกับgcc -O2 -march=nocona
ดูเหมือนว่าจะรักษาลำดับของการดำเนินการจุดลอยตัวตามที่กำหนดไว้ในแหล่ง C ++cl /O2
คอมไพเลอร์ 64 บิตจาก SDK สำหรับ Windows 7 ทำการวนลูปโดยอัตโนมัติและดูเหมือนว่าจะลองและจัดเรียงการดำเนินการเพื่อให้กลุ่มที่สามaddpd
สลับกับสามmulpd
(ดีอย่างน้อยในระบบของฉันและโปรแกรมง่าย ๆ ของฉัน) .My Core i5 750 ( สถาปัตยกรรม Nehalem ) ไม่ชอบการเพิ่มและสลับระหว่างและดูเหมือนว่าจะไม่สามารถทำงานทั้งสองแบบขนาน อย่างไรก็ตามหากจัดกลุ่มเป็น 3 มันจะทำงานได้เหมือนเวทมนตร์
สถาปัตยกรรมอื่น ๆ (อาจจะเป็นSandy Bridgeและอื่น ๆ ) ดูเหมือนว่าจะสามารถเพิ่ม / mul ในแบบคู่ขนานโดยไม่มีปัญหาหากพวกเขาสลับกันในรหัสการประกอบ
แม้ว่าจะยากที่จะยอมรับ แต่ในระบบของฉันทำงาน
cl /O2
ได้ดีขึ้นมากในการดำเนินการปรับแต่งในระดับต่ำให้กับระบบของฉันและได้รับประสิทธิภาพใกล้เคียงกับประสิทธิภาพสูงสุดสำหรับตัวอย่าง C ++ เล็กน้อยด้านบน ฉันวัดได้ระหว่าง 1.85-2.01 flops / cycle (เคยใช้ clock () ใน Windows ซึ่งไม่แม่นยำเท่านี้ฉันเดาว่าต้องใช้ตัวจับเวลาที่ดีกว่า - ขอบคุณ Mackie Messer)สิ่งที่ดีที่สุดที่ฉันจัดการด้วย
gcc
คือการวนซ้ำการเปิดและจัดเรียงการเพิ่มและการคูณในกลุ่มที่สามด้วยตนเอง เมื่อg++ -O2 -march=nocona addmul_unroll.cpp
ฉันได้ดีที่สุด0.207s, 4.825 Gflops
ซึ่งตรงกับ 1.8 flops / รอบซึ่งตอนนี้ฉันมีความสุขมาก
ในรหัส C ++ ฉันได้แทนที่for
ลูปด้วย
for (int i=0; i<loops/3; i++) {
mul1*=mul; mul2*=mul; mul3*=mul;
sum1+=add; sum2+=add; sum3+=add;
mul4*=mul; mul5*=mul; mul1*=mul;
sum4+=add; sum5+=add; sum1+=add;
mul2*=mul; mul3*=mul; mul4*=mul;
sum2+=add; sum3+=add; sum4+=add;
mul5*=mul; mul1*=mul; mul2*=mul;
sum5+=add; sum1+=add; sum2+=add;
mul3*=mul; mul4*=mul; mul5*=mul;
sum3+=add; sum4+=add; sum5+=add;
}
และตอนนี้ดูเหมือนว่าการชุมนุม
.L4:
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
mulsd xmm5, xmm3
mulsd xmm1, xmm3
mulsd xmm8, xmm3
addsd xmm10, xmm2
addsd xmm9, xmm2
addsd xmm13, xmm2
...
-funroll-loops
) ลองกับ gcc รุ่น 4.4.1 และ 4.6.2 แต่เอาต์พุต asm ดูโอเคไหม?
-O3
gcc ซึ่งเปิดใช้งาน-ftree-vectorize
หรือไม่ อาจรวมกับ-funroll-loops
แม้ว่าฉันไม่ได้ถ้ามันจำเป็นจริงๆ หลังจากนั้นการเปรียบเทียบจะดูไม่ยุติธรรมเลยหากคอมไพเลอร์ตัวใดตัวหนึ่งทำการ vectorization / unrolling ในขณะที่อันอื่นไม่ได้เพราะมันทำไม่ได้ แต่เพราะมันบอกไม่เกินไป
-funroll-loops
อาจเป็นสิ่งที่ควรลอง แต่ฉันคิดว่า-ftree-vectorize
นอกเหนือจากจุด OP กำลังพยายามเพียงรักษา 1 mul + 1 เพิ่มคำแนะนำ / รอบ คำแนะนำสามารถเป็นเซนต์คิตส์และเนวิส - มันไม่สำคัญเนื่องจากความหน่วงและปริมาณงานเท่ากัน ดังนั้นถ้าคุณสามารถรักษา 2 / รอบด้วยเซนต์คิตส์และเนวิสแล้วคุณสามารถแทนที่ด้วยเวกเตอร์ SSE และคุณจะบรรลุ 4 flops / รอบ ในคำตอบของฉันฉันแค่ทำจาก SSE -> AVX ฉันแทนที่ SSE ทั้งหมดด้วย AVX - เวลาแฝงเดียวกัน, ปริมาณงานเท่ากัน, 2x ปริมาณสัญญาณ