Bagaimana kinerja puncak teoritis dari 4 operasi floating point (presisi ganda) per siklus dapat dicapai pada CPU Intel x86-64 modern?
Sejauh yang saya mengerti, dibutuhkan tiga siklus untuk SSE add
dan lima siklus untuk mul
menyelesaikan sebagian besar CPU Intel modern (lihat misalnya 'Instruction Tables' Agner Fog ). Karena pipelining, seseorang bisa mendapatkan throughput satu add
per siklus jika algoritma memiliki setidaknya tiga penjumlahan independen. Karena itu berlaku untuk paket addpd
serta addsd
versi skalar dan register SSE dapat berisi duadouble
, throughputnya dapat sebanyak dua jepit per siklus.
Selain itu, tampaknya (walaupun saya belum melihat dokumentasi yang tepat tentang ini) add
dan mul
dapat dieksekusi secara paralel memberikan throughput maks teoretis empat jepit per siklus.
Namun, saya belum bisa meniru kinerja itu dengan program C / C ++ sederhana. Upaya terbaik saya menghasilkan sekitar 2,7 jepit / siklus. Kalau ada yang bisa berkontribusi C / C ++ atau program assembler sederhana yang menunjukkan kinerja puncak yang akan sangat dihargai.
Usaha saya:
#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;
}
Disusun dengan
g++ -O2 -march=native addmul.cpp ; ./a.out 1000
menghasilkan output berikut pada Intel Core i5-750, 2,66 GHz.
addmul: 0.270 s, 3.707 Gflops, res=1.326463
Artinya, hanya sekitar 1,4 jepit per siklus. Melihat kode assembler dengan
g++ -S -O2 -march=native -masm=intel addmul.cpp
loop utama tampaknya agak optimal bagi saya:
.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
Mengubah versi skalar dengan versi paket (addpd
dan mulpd
) akan menggandakan jumlah kegagalan tanpa mengubah waktu eksekusi dan jadi saya akan mendapatkan 2,8 flop per siklus. Apakah ada contoh sederhana yang mencapai empat jepit per siklus?
Program kecil yang bagus oleh Mysticial; inilah hasil saya (jalankan hanya untuk beberapa detik saja):
gcc -O2 -march=nocona
: 5.6 Gflops dari 10.66 Gflops (2.1 flops / cycle)cl /O2
, openmp dihapus: 10.1 Gflops dari 10.66 Gflops (3.8 flops / cycle)
Itu semua tampaknya agak rumit, tetapi kesimpulan saya sejauh ini:
gcc -O2
mengubah urutan operasi floating point independen dengan tujuan bergantianaddpd
danmulpd
jika memungkinkan. Hal yang sama berlaku untukgcc-4.6.2 -O2 -march=core2
.gcc -O2 -march=nocona
tampaknya menjaga urutan operasi floating point sebagaimana didefinisikan dalam sumber C ++.cl /O2
, kompiler 64-bit dari SDK untuk Windows 7 melakukan loop-unrolling secara otomatis dan tampaknya mencoba dan mengatur operasi sehingga kelompok-kelompokaddpd
bergantian tiga dengan tigamulpd
(baik, setidaknya pada sistem saya dan untuk program sederhana saya) .Saya Core i5 750 ( Nehalem arsitektur ) tidak seperti bolak add dan ini mul dan tampaknya tidak dapat menjalankan kedua operasi secara paralel. Namun, jika dikelompokkan dalam 3 itu tiba-tiba berfungsi seperti sihir.
Arsitektur lain (mungkin Sandy Bridge dan lainnya) tampaknya mampu mengeksekusi add / mul secara paralel tanpa masalah jika mereka berganti dalam kode assembly.
Meskipun sulit untuk diakui, tetapi pada sistem saya
cl /O2
melakukan pekerjaan yang jauh lebih baik pada operasi optimalisasi tingkat rendah untuk sistem saya dan mencapai kinerja puncak mendekati untuk contoh C ++ kecil di atas. Saya mengukur antara 1,85-2,01 jepit / siklus (telah menggunakan jam () pada Windows yang tidak tepat. Saya kira, perlu menggunakan timer yang lebih baik - terima kasih Mackie Messer).Yang terbaik yang saya kelola
gcc
adalah secara manual membuka gulungan dan mengatur penambahan dan perkalian dalam kelompok tiga. Dengang++ -O2 -march=nocona addmul_unroll.cpp
saya mendapatkan yang terbaik0.207s, 4.825 Gflops
yang sesuai dengan 1,8 jepit / siklus yang saya cukup senang dengan sekarang.
Dalam kode C ++ saya telah mengganti for
loop dengan
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;
}
Dan perakitan sekarang terlihat seperti
.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
). Sudah mencoba dengan versi gcc 4.4.1 dan 4.6.2, tetapi output asm terlihat ok?
-O3
untuk gcc, yang memungkinkan -ftree-vectorize
? Mungkin dikombinasikan dengan -funroll-loops
meskipun saya tidak tidak jika itu benar-benar diperlukan. Setelah semua perbandingan memang tampak tidak adil jika salah satu kompiler melakukan vektorisasi / membuka gulungan, sementara yang lain tidak karena itu tidak bisa, tetapi karena itu diberitahu tidak juga.
-funroll-loops
mungkin sesuatu untuk dicoba. Tapi saya pikir -ftree-vectorize
itu intinya. OP sedang mencoba hanya untuk mempertahankan 1 mul + 1 menambahkan instruksi / siklus. Instruksi dapat berupa skalar atau vektor - tidak masalah karena latensi dan throughputnya sama. Jadi jika Anda dapat mempertahankan 2 / siklus dengan skalar SSE, maka Anda dapat menggantinya dengan vektor SSE dan Anda akan mencapai 4 jepit / siklus. Dalam jawaban saya, saya melakukan hal itu dari SSE -> AVX. Saya mengganti semua SSE dengan AVX - latensi yang sama, throughput yang sama, 2x jepit.