Como eu alcanço o máximo teórico de 4 FLOPs por ciclo?

Como pode o pico máximo teórico de 4 operações de ponto flutuante (precisão dupla) por ciclo ser alcançado em um CPU Intel x86-64 moderno?

Pelo que eu entendi, são necessários três ciclos para um SSE add e cinco ciclos para que um mul seja concluído na maioria dos processadores modernos da Intel (veja, por exemplo, as “Tabelas de Instruções” da Agner Fog ). Devido ao pipelining, pode-se obter um rendimento de um sumtório por ciclo se o algoritmo tiver pelo menos três sumtórios independentes. Como isso é verdade para o addpd empacotado, assim como as versões addsd escalares e os registradores SSE podem conter duas double , a taxa de transferência pode ter até dois flops por ciclo.

Além disso, parece (embora eu não tenha visto nenhuma documentação adequada sobre isso) add e mul ‘s podem ser executados em paralelo, dando um rendimento máximo teórico de quatro flops por ciclo.

No entanto, não consegui replicar esse desempenho com um programa C / C ++ simples. Minha melhor tentativa resultou em cerca de 2,7 flops / cycle. Se alguém puder contribuir com um simples programa C / C ++ ou assembler que demonstre desempenho de pico que seria muito apreciado.

Minha tentativa:

 #include  #include  #include  #include  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 \n", argv[0]); printf("number of operations:  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; } 

Compilado com

 g++ -O2 -march=native addmul.cpp ; ./a.out 1000 

produz a seguinte saída em um processador Intel Core i5-750 de 2,66 GHz.

 addmul: 0.270 s, 3.707 Gflops, res=1.326463 

Ou seja, apenas cerca de 1,4 flops por ciclo. Olhando para o código assembler com g++ -S -O2 -march=native -masm=intel addmul.cpp o loop principal parece ótimo para mim:

 .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 

Alterar as versões escalares com versões compactadas ( addpd e mulpd ) dobraria a contagem de flop sem alterar o tempo de execução e, portanto, eu obteria pouco menos de 2.8 flops por ciclo. Existe um exemplo simples que alcança quatro flops por ciclo?

Pequeno e simpático programa de Mysticial; aqui estão os meus resultados (corra só por alguns segundos):

  • gcc -O2 -march=nocona : 5.6 Gflops de 10.66 Gflops (2.1 flops / cycle)
  • cl /O2 , openmp removido: 10.1 Gflops de 10.66 Gflops (3.8 flops / cycle)

Tudo parece um pouco complexo, mas minhas conclusões até agora:

  • gcc -O2 muda a ordem de operações de ponto flutuante independentes com o objective de alternar addpd e mulpd se possível. O mesmo se aplica ao gcc-4.6.2 -O2 -march=core2 .

  • gcc -O2 -march=nocona parece manter a ordem das operações de ponto flutuante conforme definido na fonte C ++.

  • cl /O2 , o compilador de 64 bits do SDK para o Windows 7 faz o desenrolar do loop automaticamente e parece tentar organizar as operações de modo que grupos de três addpd ‘s addpd com três mulpd (bem, pelo menos no meu sistema e para o meu programa simples).

  • Meu Core i5 750 ( arquitetura Nahelem ) não gosta de alternar add’s e mul’s e parece incapaz de executar ambas as operações em paralelo. No entanto, se agrupados em 3, de repente funciona como mágica.

  • Outras arquiteturas (possivelmente Sandy Bridge e outras) parecem poder executar add / mul em paralelo sem problemas se alternarem no código assembly.

  • Embora seja difícil de admitir, mas no meu sistema, o cl /O2 faz um trabalho muito melhor em operações de otimização de baixo nível para o meu sistema e alcança desempenho próximo ao máximo para o pequeno exemplo de C ++ acima. Eu medi entre 1.85-2.01 flops / cycle (usei clock () no Windows que não é tão preciso. Eu acho que preciso usar um timer melhor – obrigado Mackie Messer).

  • O melhor que eu consegui com o gcc foi fazer o loop manualmente e organizar adições e multiplicações em grupos de três. Com g++ -O2 -march=nocona addmul_unroll.cpp Na melhor das 0.207s, 4.825 Gflops que corresponde a 1.8 flops / cycle, com os quais estou muito feliz agora.

No código C ++ eu substituí o loop for com

  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; } 

E a assembly agora parece

 .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 ... 

Eu fiz exatamente essa tarefa antes. Mas foi principalmente para medir o consumo de energia e a temperatura da CPU. O código a seguir (que é bastante longo) atinge quase o ideal no meu Core i7 2600K.

A principal coisa a notar aqui é a enorme quantidade de desenrolamento de loop manual, bem como intercalação de multiplica e adiciona …

O projeto completo pode ser encontrado no meu GitHub: https://github.com/Mysticial/Flops

Atenção:

Se você decidir compilar e executar isso, preste atenção às temperaturas do seu processador!
Certifique-se de não superaquecer. E certifique-se de que a otimização da CPU não afeta seus resultados!

Além disso, não me responsabilizo por qualquer dano que possa resultar da execução deste código.

Notas:

  • Este código é otimizado para x64. O x86 não tem registradores suficientes para isso compilar bem.
  • Este código foi testado para funcionar bem no Visual Studio 2010/2012 e no GCC 4.6.
    ICC 11 (Intel Compiler 11) surpreendentemente tem problemas para compilá-lo bem.
  • Estes são para processadores pré-FMA. Para obter o pico de FLOPS nos processadores Intel Haswell e AMD Bulldozer (e posteriores), serão necessárias instruções FMA (Fused Multiply Add). Estes estão além do escopo deste benchmark.
 #include  #include  #include  using namespace std; typedef unsigned long long uint64; double test_dp_mac_SSE(double x,double y,uint64 iterations){ register __m128d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF; // Generate starting data. r0 = _mm_set1_pd(x); r1 = _mm_set1_pd(y); r8 = _mm_set1_pd(-0.0); r2 = _mm_xor_pd(r0,r8); r3 = _mm_or_pd(r0,r8); r4 = _mm_andnot_pd(r8,r0); r5 = _mm_mul_pd(r1,_mm_set1_pd(0.37796447300922722721)); r6 = _mm_mul_pd(r1,_mm_set1_pd(0.24253562503633297352)); r7 = _mm_mul_pd(r1,_mm_set1_pd(4.1231056256176605498)); r8 = _mm_add_pd(r0,_mm_set1_pd(0.37796447300922722721)); r9 = _mm_add_pd(r1,_mm_set1_pd(0.24253562503633297352)); rA = _mm_sub_pd(r0,_mm_set1_pd(4.1231056256176605498)); rB = _mm_sub_pd(r1,_mm_set1_pd(4.1231056256176605498)); rC = _mm_set1_pd(1.4142135623730950488); rD = _mm_set1_pd(1.7320508075688772935); rE = _mm_set1_pd(0.57735026918962576451); rF = _mm_set1_pd(0.70710678118654752440); uint64 iMASK = 0x800fffffffffffffull; __m128d MASK = _mm_set1_pd(*(double*)&iMASK); __m128d vONE = _mm_set1_pd(1.0); uint64 c = 0; while (c < iterations){ size_t i = 0; while (i < 1000){ // Here's the meat - the part that really matters. r0 = _mm_mul_pd(r0,rC); r1 = _mm_add_pd(r1,rD); r2 = _mm_mul_pd(r2,rE); r3 = _mm_sub_pd(r3,rF); r4 = _mm_mul_pd(r4,rC); r5 = _mm_add_pd(r5,rD); r6 = _mm_mul_pd(r6,rE); r7 = _mm_sub_pd(r7,rF); r8 = _mm_mul_pd(r8,rC); r9 = _mm_add_pd(r9,rD); rA = _mm_mul_pd(rA,rE); rB = _mm_sub_pd(rB,rF); r0 = _mm_add_pd(r0,rF); r1 = _mm_mul_pd(r1,rE); r2 = _mm_sub_pd(r2,rD); r3 = _mm_mul_pd(r3,rC); r4 = _mm_add_pd(r4,rF); r5 = _mm_mul_pd(r5,rE); r6 = _mm_sub_pd(r6,rD); r7 = _mm_mul_pd(r7,rC); r8 = _mm_add_pd(r8,rF); r9 = _mm_mul_pd(r9,rE); rA = _mm_sub_pd(rA,rD); rB = _mm_mul_pd(rB,rC); r0 = _mm_mul_pd(r0,rC); r1 = _mm_add_pd(r1,rD); r2 = _mm_mul_pd(r2,rE); r3 = _mm_sub_pd(r3,rF); r4 = _mm_mul_pd(r4,rC); r5 = _mm_add_pd(r5,rD); r6 = _mm_mul_pd(r6,rE); r7 = _mm_sub_pd(r7,rF); r8 = _mm_mul_pd(r8,rC); r9 = _mm_add_pd(r9,rD); rA = _mm_mul_pd(rA,rE); rB = _mm_sub_pd(rB,rF); r0 = _mm_add_pd(r0,rF); r1 = _mm_mul_pd(r1,rE); r2 = _mm_sub_pd(r2,rD); r3 = _mm_mul_pd(r3,rC); r4 = _mm_add_pd(r4,rF); r5 = _mm_mul_pd(r5,rE); r6 = _mm_sub_pd(r6,rD); r7 = _mm_mul_pd(r7,rC); r8 = _mm_add_pd(r8,rF); r9 = _mm_mul_pd(r9,rE); rA = _mm_sub_pd(rA,rD); rB = _mm_mul_pd(rB,rC); i++; } // Need to renormalize to prevent denormal/overflow. r0 = _mm_and_pd(r0,MASK); r1 = _mm_and_pd(r1,MASK); r2 = _mm_and_pd(r2,MASK); r3 = _mm_and_pd(r3,MASK); r4 = _mm_and_pd(r4,MASK); r5 = _mm_and_pd(r5,MASK); r6 = _mm_and_pd(r6,MASK); r7 = _mm_and_pd(r7,MASK); r8 = _mm_and_pd(r8,MASK); r9 = _mm_and_pd(r9,MASK); rA = _mm_and_pd(rA,MASK); rB = _mm_and_pd(rB,MASK); r0 = _mm_or_pd(r0,vONE); r1 = _mm_or_pd(r1,vONE); r2 = _mm_or_pd(r2,vONE); r3 = _mm_or_pd(r3,vONE); r4 = _mm_or_pd(r4,vONE); r5 = _mm_or_pd(r5,vONE); r6 = _mm_or_pd(r6,vONE); r7 = _mm_or_pd(r7,vONE); r8 = _mm_or_pd(r8,vONE); r9 = _mm_or_pd(r9,vONE); rA = _mm_or_pd(rA,vONE); rB = _mm_or_pd(rB,vONE); c++; } r0 = _mm_add_pd(r0,r1); r2 = _mm_add_pd(r2,r3); r4 = _mm_add_pd(r4,r5); r6 = _mm_add_pd(r6,r7); r8 = _mm_add_pd(r8,r9); rA = _mm_add_pd(rA,rB); r0 = _mm_add_pd(r0,r2); r4 = _mm_add_pd(r4,r6); r8 = _mm_add_pd(r8,rA); r0 = _mm_add_pd(r0,r4); r0 = _mm_add_pd(r0,r8); // Prevent Dead Code Elimination double out = 0; __m128d temp = r0; out += ((double*)&temp)[0]; out += ((double*)&temp)[1]; return out; } void test_dp_mac_SSE(int tds,uint64 iterations){ double *sum = (double*)malloc(tds * sizeof(double)); double start = omp_get_wtime(); #pragma omp parallel num_threads(tds) { double ret = test_dp_mac_SSE(1.1,2.1,iterations); sum[omp_get_thread_num()] = ret; } double secs = omp_get_wtime() - start; uint64 ops = 48 * 1000 * iterations * tds * 2; cout << "Seconds = " << secs << endl; cout << "FP Ops = " << ops << endl; cout << "FLOPs = " << ops / secs << endl; double out = 0; int c = 0; while (c < tds){ out += sum[c++]; } cout << "sum = " << out << endl; cout << endl; free(sum); } int main(){ // (threads, iterations) test_dp_mac_SSE(8,10000000); system("pause"); } 

Saída (1 thread, 10000000 iterações) - Compilado com o Visual Studio 2010 SP1 - x64 Release:

 Seconds = 55.5104 FP Ops = 960000000000 FLOPs = 1.7294e+010 sum = 2.22652 

A máquina é um Core i7 2600K a 4,4 GHz. O pico teórico de SSE é de 4 flops * 4,4 GHz = 17,6 GFlops . Este código atinge 17,3 GFlops - nada mal.

Saída (8 threads, 10000000 iterações) - Compilado com o Visual Studio 2010 SP1 - x64 Release:

 Seconds = 117.202 FP Ops = 7680000000000 FLOPs = 6.55279e+010 sum = 17.8122 

O pico teórico de SSE é de 4 flops * 4 colors * 4,4 GHz = 70,4 GFlops. Real é 65,5 GFlops .


Vamos dar um passo adiante. AVX ...

 #include  #include  #include  using namespace std; typedef unsigned long long uint64; double test_dp_mac_AVX(double x,double y,uint64 iterations){ register __m256d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF; // Generate starting data. r0 = _mm256_set1_pd(x); r1 = _mm256_set1_pd(y); r8 = _mm256_set1_pd(-0.0); r2 = _mm256_xor_pd(r0,r8); r3 = _mm256_or_pd(r0,r8); r4 = _mm256_andnot_pd(r8,r0); r5 = _mm256_mul_pd(r1,_mm256_set1_pd(0.37796447300922722721)); r6 = _mm256_mul_pd(r1,_mm256_set1_pd(0.24253562503633297352)); r7 = _mm256_mul_pd(r1,_mm256_set1_pd(4.1231056256176605498)); r8 = _mm256_add_pd(r0,_mm256_set1_pd(0.37796447300922722721)); r9 = _mm256_add_pd(r1,_mm256_set1_pd(0.24253562503633297352)); rA = _mm256_sub_pd(r0,_mm256_set1_pd(4.1231056256176605498)); rB = _mm256_sub_pd(r1,_mm256_set1_pd(4.1231056256176605498)); rC = _mm256_set1_pd(1.4142135623730950488); rD = _mm256_set1_pd(1.7320508075688772935); rE = _mm256_set1_pd(0.57735026918962576451); rF = _mm256_set1_pd(0.70710678118654752440); uint64 iMASK = 0x800fffffffffffffull; __m256d MASK = _mm256_set1_pd(*(double*)&iMASK); __m256d vONE = _mm256_set1_pd(1.0); uint64 c = 0; while (c < iterations){ size_t i = 0; while (i < 1000){ // Here's the meat - the part that really matters. r0 = _mm256_mul_pd(r0,rC); r1 = _mm256_add_pd(r1,rD); r2 = _mm256_mul_pd(r2,rE); r3 = _mm256_sub_pd(r3,rF); r4 = _mm256_mul_pd(r4,rC); r5 = _mm256_add_pd(r5,rD); r6 = _mm256_mul_pd(r6,rE); r7 = _mm256_sub_pd(r7,rF); r8 = _mm256_mul_pd(r8,rC); r9 = _mm256_add_pd(r9,rD); rA = _mm256_mul_pd(rA,rE); rB = _mm256_sub_pd(rB,rF); r0 = _mm256_add_pd(r0,rF); r1 = _mm256_mul_pd(r1,rE); r2 = _mm256_sub_pd(r2,rD); r3 = _mm256_mul_pd(r3,rC); r4 = _mm256_add_pd(r4,rF); r5 = _mm256_mul_pd(r5,rE); r6 = _mm256_sub_pd(r6,rD); r7 = _mm256_mul_pd(r7,rC); r8 = _mm256_add_pd(r8,rF); r9 = _mm256_mul_pd(r9,rE); rA = _mm256_sub_pd(rA,rD); rB = _mm256_mul_pd(rB,rC); r0 = _mm256_mul_pd(r0,rC); r1 = _mm256_add_pd(r1,rD); r2 = _mm256_mul_pd(r2,rE); r3 = _mm256_sub_pd(r3,rF); r4 = _mm256_mul_pd(r4,rC); r5 = _mm256_add_pd(r5,rD); r6 = _mm256_mul_pd(r6,rE); r7 = _mm256_sub_pd(r7,rF); r8 = _mm256_mul_pd(r8,rC); r9 = _mm256_add_pd(r9,rD); rA = _mm256_mul_pd(rA,rE); rB = _mm256_sub_pd(rB,rF); r0 = _mm256_add_pd(r0,rF); r1 = _mm256_mul_pd(r1,rE); r2 = _mm256_sub_pd(r2,rD); r3 = _mm256_mul_pd(r3,rC); r4 = _mm256_add_pd(r4,rF); r5 = _mm256_mul_pd(r5,rE); r6 = _mm256_sub_pd(r6,rD); r7 = _mm256_mul_pd(r7,rC); r8 = _mm256_add_pd(r8,rF); r9 = _mm256_mul_pd(r9,rE); rA = _mm256_sub_pd(rA,rD); rB = _mm256_mul_pd(rB,rC); i++; } // Need to renormalize to prevent denormal/overflow. r0 = _mm256_and_pd(r0,MASK); r1 = _mm256_and_pd(r1,MASK); r2 = _mm256_and_pd(r2,MASK); r3 = _mm256_and_pd(r3,MASK); r4 = _mm256_and_pd(r4,MASK); r5 = _mm256_and_pd(r5,MASK); r6 = _mm256_and_pd(r6,MASK); r7 = _mm256_and_pd(r7,MASK); r8 = _mm256_and_pd(r8,MASK); r9 = _mm256_and_pd(r9,MASK); rA = _mm256_and_pd(rA,MASK); rB = _mm256_and_pd(rB,MASK); r0 = _mm256_or_pd(r0,vONE); r1 = _mm256_or_pd(r1,vONE); r2 = _mm256_or_pd(r2,vONE); r3 = _mm256_or_pd(r3,vONE); r4 = _mm256_or_pd(r4,vONE); r5 = _mm256_or_pd(r5,vONE); r6 = _mm256_or_pd(r6,vONE); r7 = _mm256_or_pd(r7,vONE); r8 = _mm256_or_pd(r8,vONE); r9 = _mm256_or_pd(r9,vONE); rA = _mm256_or_pd(rA,vONE); rB = _mm256_or_pd(rB,vONE); c++; } r0 = _mm256_add_pd(r0,r1); r2 = _mm256_add_pd(r2,r3); r4 = _mm256_add_pd(r4,r5); r6 = _mm256_add_pd(r6,r7); r8 = _mm256_add_pd(r8,r9); rA = _mm256_add_pd(rA,rB); r0 = _mm256_add_pd(r0,r2); r4 = _mm256_add_pd(r4,r6); r8 = _mm256_add_pd(r8,rA); r0 = _mm256_add_pd(r0,r4); r0 = _mm256_add_pd(r0,r8); // Prevent Dead Code Elimination double out = 0; __m256d temp = r0; out += ((double*)&temp)[0]; out += ((double*)&temp)[1]; out += ((double*)&temp)[2]; out += ((double*)&temp)[3]; return out; } void test_dp_mac_AVX(int tds,uint64 iterations){ double *sum = (double*)malloc(tds * sizeof(double)); double start = omp_get_wtime(); #pragma omp parallel num_threads(tds) { double ret = test_dp_mac_AVX(1.1,2.1,iterations); sum[omp_get_thread_num()] = ret; } double secs = omp_get_wtime() - start; uint64 ops = 48 * 1000 * iterations * tds * 4; cout << "Seconds = " << secs << endl; cout << "FP Ops = " << ops << endl; cout << "FLOPs = " << ops / secs << endl; double out = 0; int c = 0; while (c < tds){ out += sum[c++]; } cout << "sum = " << out << endl; cout << endl; free(sum); } int main(){ // (threads, iterations) test_dp_mac_AVX(8,10000000); system("pause"); } 

Saída (1 thread, 10000000 iterações) - Compilado com o Visual Studio 2010 SP1 - x64 Release:

 Seconds = 57.4679 FP Ops = 1920000000000 FLOPs = 3.34099e+010 sum = 4.45305 

O pico teórico de AVX é de 8 falhanços * 4,4 GHz = 35,2 GFlops . Real é 33,4 GFlops .

Saída (8 threads, 10000000 iterações) - Compilado com o Visual Studio 2010 SP1 - x64 Release:

 Seconds = 111.119 FP Ops = 15360000000000 FLOPs = 1.3823e+011 sum = 35.6244 

O pico teórico de AVX é de 8 flops * 4 colors * 4,4 GHz = 140,8 GFlops. Real é 138,2 GFlops .


Agora, para algumas explicações:

A parte crítica de desempenho é obviamente as 48 instruções dentro do loop interno. Você notará que é dividido em 4 blocos de 12 instruções cada. Cada um desses 12 blocos de instruções é completamente independente um do outro - e leva em média 6 ciclos para ser executado.

Portanto, há 12 instruções e 6 ciclos entre o assunto a ser usado. A latência da multiplicação é de 5 ciclos, por isso é suficiente para evitar paradas de latência.

A etapa de normalização é necessária para manter os dados acima / abaixo do stream. Isso é necessário, pois o código de não fazer nada aumentará / diminuirá lentamente a magnitude dos dados.

Portanto, é realmente possível fazer melhor do que isso se você usar todos os zeros e se livrar da etapa de normalização. No entanto, desde que escrevi o benchmark para medir o consumo de energia e a temperatura, tive que garantir que os flops estivessem em dados "reais", em vez de zeros - já que as unidades de execução podem muito bem lidar com zeros que usam menos energia e produzir menos calor.


Mais resultados:

  • Intel Core i7 920 a 3,5 GHz
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1 - versão x64

Tópicos: 1

 Seconds = 72.1116 FP Ops = 960000000000 FLOPs = 1.33127e+010 sum = 2.22652 

Pico Teórico de SSE: 4 flops * 3,5 GHz = 14,0 GFlops . Real é 13,3 GFlops .

Tópicos: 8

 Seconds = 149.576 FP Ops = 7680000000000 FLOPs = 5.13452e+010 sum = 17.8122 

Pico Teórico de SSE: 4 flops * 4 colors * 3.5 GHz = 56.0 GFlops . Real é 51,3 GFlops .

Meus tempos de processamento atingiram 76C na corrida multi-thread! Se você executar esses, certifique-se de que os resultados não sejam afetados pela aceleração da CPU.


  • 2 x Intel Xeon X5482 Harpertown a 3,2 GHz
  • Ubuntu Linux 10 x64
  • GCC 4.5.2 x64 - (-O2 -msse3 -fopenmp)

Tópicos: 1

 Seconds = 78.3357 FP Ops = 960000000000 FLOPs = 1.22549e+10 sum = 2.22652 

Pico teórico de SSE: 4 flops * 3,2 GHz = 12,8 GFlops . Real é 12,3 GFlops .

Tópicos: 8

 Seconds = 78.4733 FP Ops = 7680000000000 FLOPs = 9.78676e+10 sum = 17.8122 

Pico Teórico de SSE: 4 flops * 8 colors * 3.2 GHz = 102.4 GFlops . Real é 97,9 GFlops .

Há um ponto na arquitetura Intel que muitas vezes as pessoas esquecem, as portas de despacho são compartilhadas entre Int e FP / SIMD. Isso significa que você só receberá uma certa quantidade de rajadas de FP / SIMD antes que a lógica de loop crie bolhas em seu stream de ponto flutuante. Mystical conseguiu mais flops do seu código, porque usou passos mais longos em seu loop desenrolado.

Se você olhar para a arquitetura do Nehalem / Sandy Bridge aqui http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 , é bem claro o que acontece.

Em contraste, deve ser mais fácil atingir o desempenho máximo na AMD (Bulldozer), pois os tubos INT e FP / SIMD têm portas de problemas separadas com seu próprio agendador.

Isso é apenas teórico, pois não tenho nenhum desses processadores para testar.

Filiais podem definitivamente impedir você de sustentar o desempenho teórico máximo. Você vê alguma diferença se manualmente fizer algum desenrolamento de loop? Por exemplo, se você colocar 5 ou 10 vezes mais operações por iteração de loop:

 for(int i=0; i 

Usando o Intels icc Versão 11.1 em um Intel Core 2 Duo de 2.4GHz eu recebo

 Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000 addmul: 0.105 s, 9.525 Gflops, res=0.000000 Macintosh:~ mackie$ icc -v Version 11.1 

Isso é muito próximo dos 9,6 Gflops ideais.

EDITAR:

Ops, olhando para o código de assembly, parece que o icc não apenas vetorizou a multiplicação, mas também extraiu as adições do loop. Forçando uma semântica fp mais rigorosa, o código não é mais vetorizado:

 Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000 addmul: 0.516 s, 1.938 Gflops, res=1.326463 

EDIT2:

Como pedido:

 Macintosh:~ mackie$ clang -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000 addmul: 0.209 s, 4.786 Gflops, res=1.326463 Macintosh:~ mackie$ clang -v Apple clang version 3.0 (tags/Apple/clang-211.10.1) (based on LLVM 3.0svn) Target: x86_64-apple-darwin11.2.0 Thread model: posix 

O loop interno do código do clang se parece com isso:

  .align 4, 0x90 LBB2_4: ## =>This Inner Loop Header: Depth=1 addsd %xmm2, %xmm3 addsd %xmm2, %xmm14 addsd %xmm2, %xmm5 addsd %xmm2, %xmm1 addsd %xmm2, %xmm4 mulsd %xmm2, %xmm0 mulsd %xmm2, %xmm6 mulsd %xmm2, %xmm7 mulsd %xmm2, %xmm11 mulsd %xmm2, %xmm13 incl %eax cmpl %r14d, %eax jl LBB2_4 

EDIT3:

Finalmente, duas sugestões: Primeiro, se você gosta desse tipo de benchmarking, considere usar a instrução gettimeofday(2) em gettimeofday(2) . É muito mais preciso e entrega o tempo em ciclos, que geralmente é o que você está interessado de qualquer maneira. Para gcc e amigos, você pode defini-lo assim:

 #include  static __inline__ uint64_t rdtsc(void) { uint64_t rval; __asm__ volatile ("rdtsc" : "=A" (rval)); return rval; } 

Segundo, você deve executar seu programa de benchmark várias vezes e usar apenas o melhor desempenho . Nos sistemas operacionais modernos, muitas coisas acontecem em paralelo, a CPU pode estar em um modo de economia de energia de baixa freqüência, etc. A execução repetida do programa fornece um resultado mais próximo do ideal.