Как мне достичь теоретического максимума 4 FLOP за цикл?
Как достичь теоретической пиковой производительности 4 операций с плавающей запятой (двойной точности) за такт на современном процессоре Intel x86-64?
Насколько я понимаю, это займет три цикла для SSE add
и пять циклов для mul
завершить на большинстве современных процессоров Intel (см., например , "Таблицы инструкций" Агнера Фога). Благодаря конвейерной обработке можно получить пропускную способность одного add
за цикл, если алгоритм имеет не менее трех независимых сумм. Так как это верно для упакованных addpd
а также скаляр addsd
версии и регистры SSE могут содержать два double
Пропускная способность может достигать двух флопов за цикл.
Кроме того, кажется (хотя я не видел надлежащей документации по этому вопросу) add
и mul
Это может быть выполнено параллельно, давая теоретическую максимальную пропускную способность в четыре флопа за цикл.
Однако я не смог воспроизвести эту производительность с помощью простой программы на C/C++. Моя лучшая попытка привела к примерно 2,7 флопс / цикл. Если кто-то может предложить простую 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 ГГц.
addmul: 0.270 s, 3.707 Gflops, res=1.326463
То есть примерно 1,4 флопа за цикл. Глядя на ассемблерный код с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
) удваивает количество флопов без изменения времени выполнения, поэтому мне не хватает 2,8 флопов за цикл. Есть ли простой пример, который достигает четырех флопов за цикл?
Хорошая маленькая программа от Mysticial; Вот мои результаты (хотя бы на несколько секунд):
gcc -O2 -march=nocona
: 5,6 Гфлоп из 10,66 Гфлоп (2,1 флоп / цикл)cl /O2
openmp удалено: 10,1 Гфлоп из 10,66 Гфлоп (3,8 Флоп / цикл)
Все это кажется немного сложным, но мои выводы пока:
gcc -O2
изменяет порядок независимых операций с плавающей запятой с целью чередованияaddpd
а такжеmulpd
если возможно. То же относится и кgcc-4.6.2 -O2 -march=core2
,gcc -O2 -march=nocona
похоже, сохраняет порядок операций с плавающей запятой, как определено в источнике C++.cl /O2
64-разрядный компилятор из SDK для Windows 7 выполняет автоматическое развертывание цикла и, по-видимому, пытается упорядочить операции таким образом, чтобы группы по триaddpd
чередуется с тремяmulpd
(ну, по крайней мере, в моей системе и для моей простой программы).Мой Core i5 750 ( архитектура Nahelem) не любит чередование надстроек и мул, и, кажется, не может выполнять обе операции параллельно. Тем не менее, если сгруппированы в 3-х, это внезапно работает как магия.
Другие архитектуры (возможно, Sandy Bridge и другие) могут без проблем выполнять add / mul параллельно, если они чередуются в коде сборки.
Хотя это трудно признать, но на моей системе
cl /O2
гораздо лучше справляется с низкоуровневыми операциями оптимизации для моей системы и достигает почти максимальной производительности для небольшого примера C++, приведенного выше. Я измерял между 1,85-2,01 флопс / цикл (использовал clock() в Windows, что не так точно. Я думаю, нужно использовать лучший таймер - спасибо Mackie Messer).Лучшее, с чем я справился
gcc
должен был вручную развернуть цикл и расставить сложения и умножения в группы по три. Сg++ -O2 -march=nocona addmul_unroll.cpp
Я получаю в лучшем случае0.207s, 4.825 Gflops
что соответствует 1,8 флопс / цикл, что меня вполне устраивает сейчас.
В коде 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
...
4 ответа
Я выполнил эту задачу раньше. Но это было главным образом для измерения энергопотребления и температуры процессора. Следующий код (который является довольно длинным) достигает почти оптимального на моем Core i7 2600K.
Ключевым моментом, который следует здесь отметить, является огромное количество ручных циклических развертываний, а также чередование умножений и добавлений...
Полный проект можно найти на моем GitHub: https://github.com/Mysticial/Flops
Предупреждение:
Если вы решили скомпилировать и запустить это, обратите внимание на температуру вашего процессора!!!
Убедитесь, что вы не перегреваете его. И убедитесь, что удушение процессора не влияет на ваши результаты!
Кроме того, я не несу ответственности за любой ущерб, который может возникнуть в результате выполнения этого кода.
Заметки:
- Этот код оптимизирован для x64. x86 не имеет достаточно регистров для этого, чтобы хорошо скомпилировать.
- Этот код был протестирован для правильной работы в Visual Studio 2010/2012 и GCC 4.6.
На удивление, ICC 11 (Intel Compiler 11) не может скомпилировать его. - Они предназначены для процессоров до FMA. Для достижения пиковых значений FLOPS на процессорах Intel Haswell и AMD Bulldozer (и более поздних) потребуются инструкции FMA (Fused Multiply Add). Это выходит за рамки этого теста.
#include <emmintrin.h>
#include <omp.h>
#include <iostream>
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");
}
Вывод (1 поток, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Seconds = 55.5104
FP Ops = 960000000000
FLOPs = 1.7294e+010
sum = 2.22652
Машина представляет собой Core i7 2600K @ 4,4 ГГц. Теоретический пик SSE составляет 4 флопа * 4,4 ГГц = 17,6 Гфлопс. Этот код достигает 17,3 GFlops - неплохо.
Вывод (8 потоков, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Seconds = 117.202
FP Ops = 7680000000000
FLOPs = 6.55279e+010
sum = 17.8122
Теоретический пик SSE составляет 4 флопа * 4 ядра * 4,4 ГГц = 70,4 Гфлопса. Фактически это 65,5 GFlops.
Давайте сделаем еще один шаг вперед. AVX...
#include <immintrin.h>
#include <omp.h>
#include <iostream>
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");
}
Вывод (1 поток, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Seconds = 57.4679
FP Ops = 1920000000000
FLOPs = 3.34099e+010
sum = 4.45305
Теоретический пик AVX составляет 8 флопов * 4,4 ГГц = 35,2 Гфлопс. Фактический 33,4 GFlops.
Вывод (8 потоков, 10000000 итераций) - скомпилировано с Visual Studio 2010 SP1 - выпуск x64:
Seconds = 111.119
FP Ops = 15360000000000
FLOPs = 1.3823e+011
sum = 35.6244
Теоретический пик AVX составляет 8 флопов * 4 ядра * 4,4 ГГц = 140,8 Гфлопс. Фактически это 138,2 GFlops.
Теперь несколько пояснений:
Критическая часть производительности - это, очевидно, 48 инструкций во внутреннем цикле. Вы заметите, что он разбит на 4 блока по 12 инструкций в каждом. Каждый из этих 12 блоков инструкций полностью независим друг от друга - для выполнения в среднем требуется 6 циклов.
Таким образом, существует 12 инструкций и 6 циклов между выпусками. Задержка умножения составляет 5 циклов, так что этого достаточно, чтобы избежать задержек задержки.
Шаг нормализации необходим, чтобы предотвратить переполнение / переполнение данных. Это необходимо, поскольку беспроигрышный код будет медленно увеличивать / уменьшать величину данных.
Так что на самом деле можно добиться большего, чем это, если вы просто используете все нули и избавляетесь от шага нормализации. Однако, поскольку я написал эталон для измерения энергопотребления и температуры, я должен был убедиться, что на флопах были "реальные" данные, а не нули - поскольку исполнительные блоки вполне могут иметь особую обработку случая для нулей, которые потребляют меньше энергии и производить меньше тепла.
Больше результатов:
- Intel Core i7 920 @ 3,5 ГГц
- Windows 7 Ultimate x64
- Visual Studio 2010 SP1 - выпуск x64
Темы: 1
Seconds = 72.1116
FP Ops = 960000000000
FLOPs = 1.33127e+010
sum = 2.22652
Теоретический пик SSE: 4 флопа * 3,5 ГГц = 14,0 гфлопс. Фактически это 13,3 GFlops.
Темы: 8
Seconds = 149.576
FP Ops = 7680000000000
FLOPs = 5.13452e+010
sum = 17.8122
Теоретический пик SSE: 4 флопа * 4 ядра * 3,5 ГГц = 56,0 Гфлопса. Фактический 51,3 GFlops.
В многопоточном режиме температура моего процессора достигла 76C! Если вы запускаете их, убедитесь, что на результаты не влияет регулирование процессора.
- 2 х Intel Xeon X5482 Harpertown @ 3,2 ГГц
- Ubuntu Linux 10 x64
- GCC 4.5.2 x64 - (-O2 -msse3 -fopenmp)
Темы: 1
Seconds = 78.3357
FP Ops = 960000000000
FLOPs = 1.22549e+10
sum = 2.22652
Теоретический пик SSE: 4 флопа * 3,2 ГГц = 12,8 гфлопс. Фактически это 12,3 GFlops.
Темы: 8
Seconds = 78.4733
FP Ops = 7680000000000
FLOPs = 9.78676e+10
sum = 17.8122
Теоретический пик SSE: 4 флопа * 8 ядер * 3,2 ГГц = 102,4 Гфлопса. Фактически это 97,9 GFlops.
В архитектуре Intel есть один момент, о котором люди часто забывают: порты диспетчеризации разделяются между Int и FP/SIMD. Это означает, что вы получите только определенное количество пакетов FP / SIMD, прежде чем логика цикла создаст пузырьки в потоке с плавающей запятой. Mystical получил больше провалов из своего кода, потому что он использовал более длинные шаги в своем развернутом цикле.
Если вы посмотрите на архитектуру Nehalem/Sandy Bridge здесь http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 то совершенно ясно, что происходит.
Напротив, должно быть проще достичь пиковой производительности на AMD (Bulldozer), так как каналы INT и FP / SIMD имеют отдельные порты выдачи с собственным планировщиком.
Это только теоретически, поскольку у меня нет ни одного из этих процессоров для тестирования.
Филиалы определенно могут помешать вам поддерживать максимальную теоретическую производительность. Видите ли вы разницу, если вы выполняете ручное развертывание? Например, если вы поместили в 5 или 10 раз больше операций на цикл итерации:
for(int i=0; i<loops/5; i++) {
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
}
Использование Intel ICC версии 11.1 на 2,4 ГГц Intel Core 2 Duo я получаю
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
Это очень близко к идеальным 9,6 Гфлопс.
РЕДАКТИРОВАТЬ:
Ой, глядя на код сборки, кажется, что icc не только векторизовал умножение, но и вытащил дополнения из цикла. При навязывании более строгой семантики fp код больше не векторизован:
Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000
addmul: 0.516 s, 1.938 Gflops, res=1.326463
EDIT2:
Как просили:
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
Внутренний цикл кода Clang выглядит так:
.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:
Наконец, два предложения: во-первых, если вам нравится этот тип тестирования, рассмотрите возможность использования rdtsc
инструкция стоит gettimeofday(2)
, Это намного точнее и обеспечивает время в циклах, что обычно так или иначе вас интересует. Для gcc и друзей вы можете определить это так:
#include <stdint.h>
static __inline__ uint64_t rdtsc(void)
{
uint64_t rval;
__asm__ volatile ("rdtsc" : "=A" (rval));
return rval;
}
Во-вторых, вам следует несколько раз запускать свою тестовую программу и использовать только наилучшую производительность. В современных операционных системах многие вещи происходят параллельно, процессор может находиться в режиме энергосбережения на низких частотах и т. Д. Многократный запуск программы дает результат, близкий к идеальному случаю.