Làm thế nào có thể đạt được hiệu suất cao nhất về mặt lý thuyết của 4 thao tác điểm nổi (độ chính xác kép) trên mỗi chu kỳ trên CPU Intel x86-64 hiện đại?
Theo tôi hiểu, phải mất ba chu kỳ cho một SSE add
và năm chu kỳ mul
để hoàn thành trên hầu hết các CPU Intel hiện đại (xem ví dụ 'Bảng hướng dẫn' của Agner Fog ). Do đường ống, người ta có thể nhận được thông lượng một add
lần trong mỗi chu kỳ nếu thuật toán có ít nhất ba phép tính tổng độc lập. Vì điều đó đúng với addpd
các addsd
phiên bản đóng gói cũng như các thanh ghi vô hướng và các thanh ghi SSE có thể chứa hai double
thông lượng có thể bằng hai flop mỗi chu kỳ.
Hơn nữa, dường như (mặc dù tôi chưa thấy bất kỳ tài liệu phù hợp nào về vấn đề này) add
và mul
có thể được thực thi song song với thông lượng tối đa về mặt lý thuyết là bốn flops mỗi chu kỳ.
Tuy nhiên, tôi không thể sao chép hiệu suất đó bằng chương trình C / C ++ đơn giản. Nỗ lực tốt nhất của tôi dẫn đến khoảng 2,7 flops / chu kỳ. Nếu bất cứ ai cũng có thể đóng góp một chương trình C / C ++ hoặc trình biên dịch chương trình biên dịch đơn giản thể hiện hiệu suất cao nhất sẽ được đánh giá cao.
Nỗ lực của tôi:
#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;
}
Tổng hợp với
g++ -O2 -march=native addmul.cpp ; ./a.out 1000
tạo ra đầu ra sau trên Intel Core i5-750, 2,66 GHz.
addmul: 0.270 s, 3.707 Gflops, res=1.326463
Đó là, chỉ khoảng 1,4 flops mỗi chu kỳ. Nhìn vào mã trình biên dịch với
g++ -S -O2 -march=native -masm=intel addmul.cpp
vòng lặp chính có vẻ tối ưu với tôi:
.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
Thay đổi các phiên bản vô hướng với các phiên bản đóng gói ( addpd
và mulpd
) sẽ tăng gấp đôi số lượng flop mà không thay đổi thời gian thực hiện và vì vậy tôi chỉ nhận được 2,8 flop mỗi chu kỳ. Có một ví dụ đơn giản mà đạt được bốn flops mỗi chu kỳ?
Chương trình nhỏ xinh của Mysticial; đây là kết quả của tôi (chỉ chạy trong vài giây):
gcc -O2 -march=nocona
: 5,6 Gflops trong số 10,66 Gflops (2,1 flops / chu kỳ)cl /O2
, openmp đã bị xóa: 10.1 Gflops trong số 10,66 Gflops (3,8 flops / chu kỳ)
Tất cả có vẻ hơi phức tạp, nhưng kết luận của tôi cho đến nay:
gcc -O2
thay đổi thứ tự của các phép toán dấu phẩy động độc lập với mục đích xen kẽaddpd
vàmulpd
nếu có thể. Áp dụng tương tự chogcc-4.6.2 -O2 -march=core2
.gcc -O2 -march=nocona
dường như giữ nguyên thứ tự của các phép toán dấu phẩy động như được định nghĩa trong nguồn C ++.cl /O2
, trình biên dịch 64 bit từ SDK cho Windows 7 sẽ tự động hủy vòng lặp và dường như thử và sắp xếp các hoạt động sao cho các nhóm baaddpd
thay thế với bamulpd
(ít nhất là trên hệ thống của tôi và cho chương trình đơn giản của tôi) .My Core i5 750 ( kiến trúc Nehalem ) không thích xen kẽ các add và mul và dường như không thể chạy song song cả hai hoạt động. Tuy nhiên, nếu được nhóm thành 3 thì nó đột nhiên hoạt động như ma thuật.
Các kiến trúc khác (có thể là Sandy Bridge và các công trình khác) dường như có thể thực thi song song add / mul mà không gặp vấn đề gì nếu chúng thay thế trong mã lắp ráp.
Mặc dù khó thừa nhận, nhưng trên hệ thống của tôi
cl /O2
thực hiện công việc tốt hơn nhiều ở các hoạt động tối ưu hóa ở mức độ thấp cho hệ thống của tôi và đạt được hiệu suất cao nhất cho ví dụ C ++ nhỏ ở trên. Tôi đo được giữa 1,85-2,01 flops / chu kỳ (đã sử dụng đồng hồ () trong Windows không chính xác. Tôi đoán, cần sử dụng bộ hẹn giờ tốt hơn - cảm ơn Mackie Messer).Cách tốt nhất tôi quản lý
gcc
là lặp lại thủ công hủy đăng ký và sắp xếp các phép cộng và phép nhân trong các nhóm ba. Vớig++ -O2 -march=nocona addmul_unroll.cpp
tôi nhận được tốt nhất0.207s, 4.825 Gflops
tương ứng với 1,8 flops / chu kỳ mà bây giờ tôi khá hài lòng.
Trong mã C ++, tôi đã thay thế for
vòng lặp bằng
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;
}
Và lắp ráp bây giờ trông giống như
.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
). Đã thử với gcc phiên bản 4.4.1 và 4.6.2, nhưng đầu ra asm có vẻ ổn?
-O3
cho gcc, cho phép -ftree-vectorize
? Có lẽ kết hợp với -funroll-loops
mặc dù tôi không nếu điều đó thực sự cần thiết. Sau đó, việc so sánh có vẻ không công bằng nếu một trong các trình biên dịch thực hiện vector hóa / không kiểm soát, trong khi cái kia không phải vì nó không thể, nhưng vì nó không được nói quá.
-funroll-loops
có lẽ là một cái gì đó để thử. Nhưng tôi nghĩ -ftree-vectorize
là bên cạnh quan điểm. OP đang cố gắng duy trì 1 mul + 1 thêm hướng dẫn / chu kỳ. Các hướng dẫn có thể là vô hướng hoặc vectơ - không thành vấn đề vì độ trễ và thông lượng là như nhau. Vì vậy, nếu bạn có thể duy trì 2 / chu kỳ với SSE vô hướng, thì bạn có thể thay thế chúng bằng vector SSE và bạn sẽ đạt được 4 flops / chu kỳ. Trong câu trả lời của tôi, tôi đã làm điều đó từ SSE -> AVX. Tôi đã thay thế tất cả SSE bằng AVX - cùng độ trễ, cùng thông lượng, gấp đôi flops.