이론적으로 사이클 당 최대 4 개의 FLOP을 달성하려면 어떻게해야합니까?
최신 x86-64 Intel CPU에서 사이클 당 4 개의 부동 소수점 연산 (배 정밀도)의 이론적 최고 성능을 달성하려면 어떻게해야합니까?
내가 이해하는 한 SSE 는 3주기 add
, a mul
는 대부분의 최신 Intel CPU에서 완료 하는 데 5주기 가 걸립니다 (예를 들어 Agner Fog의 'Instruction Tables'참조 ). 파이프 라이닝으로 인해 add
알고리즘에 3 개 이상의 독립적 인 합계가있는 경우 사이클 당 하나의 처리량을 얻을 수 있습니다 . 이는 패킹 된 버전과 addpd
스칼라 addsd
버전에 해당되며 SSE 레지스터에 2 개의 값이 포함될 double
수 있으므로 처리량은 사이클 당 최대 2 개의 플롭이 될 수 있습니다.
또한, (이에 대한 적절한 문서를 보지 못했지만) add
's와 mul
's는 병렬로 실행되어 이론적으로 사이클 당 4 플롭의 최대 처리량을 제공 할 수 있습니다.
그러나 간단한 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.66GHz에서 다음 출력을 생성합니다.
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 플롭이 부족합니다. 사이클 당 4 플롭을 달성하는 간단한 예가 있습니까?
Mysticial의 멋진 작은 프로그램; 내 결과는 다음과 같습니다 (몇 초 동안 실행).
gcc -O2 -march=nocona
: 10.66Gflops 중 5.6Gflops (2.1 플롭 / 사이클)cl /O2
, openmp 제거 : 10.66 Gflops 중 10.1 Gflops (3.8 flops / cycle)
모든 것이 조금 복잡해 보이지만 지금까지 내 결론은 다음과 같습니다.
gcc -O2
가능한 경우addpd
와 를 번갈아 가며 독립적 인 부동 소수점 연산의 순서를 변경mulpd
합니다. 에도 동일하게 적용됩니다gcc-4.6.2 -O2 -march=core2
.gcc -O2 -march=nocona
C ++ 소스에 정의 된대로 부동 소수점 연산의 순서를 유지하는 것 같습니다.cl /O2
, Windows 7 용 SDK 의 64 비트 컴파일러는 자동으로 루프 언 롤링을 수행하고 작업을 시도하고 정렬하여 3 개의 그룹이 3 개의 그룹으로addpd
번갈아 가도록하는 것 같습니다mulpd
(적어도 내 시스템과 간단한 프로그램에서는) .내 Core i5 750 ( Nehalem 아키텍처 )은 add와 mul을 번갈아 사용하는 것을 좋아하지 않으며 두 작업을 병렬로 실행할 수없는 것 같습니다. 그러나 3으로 묶으면 갑자기 마술처럼 작동합니다.
다른 아키텍처 ( Sandy Bridge 및 기타)는 어셈블리 코드에서 번갈아 가며 문제없이 병렬로 add / mul을 실행할 수있는 것으로 보입니다.
인정하기는 어렵지만 내 시스템
cl /O2
에서는 저수준의 시스템 최적화 작업에서 훨씬 더 나은 작업을 수행하고 위의 작은 C ++ 예제에서 최대 성능에 가깝습니다. 나는 1.85-2.01 플롭 / 사이클 사이를 측정했다 (정확하지 않은 Windows에서 clock ()을 사용했다. 더 나은 타이머를 사용해야한다고 생각한다-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
...
나는 전에이 정확한 작업을 수행했습니다. 그러나 주로 전력 소비와 CPU 온도를 측정하는 것이 었습니다. 다음 코드 (상당히 긴)는 Core i7 2600K에서 최적에 가깝습니다.
여기서 주목해야 할 핵심 사항은 엄청난 양의 수동 루프 풀기 및 곱셈 및 추가의 인터리빙입니다.
전체 프로젝트는 내 GitHub에서 찾을 수 있습니다 : https://github.com/Mysticial/Flops
경고:
이것을 컴파일하고 실행하기로 결정했다면 CPU 온도에주의하십시오 !!!
과열하지 않도록하십시오. 그리고 CPU 스로틀 링이 결과에 영향을 미치지 않는지 확인하십시오!
또한이 코드를 실행하여 발생할 수있는 손상에 대해 책임을지지 않습니다.
메모:
- 이 코드는 x64에 최적화되어 있습니다. x86은 이것을 잘 컴파일하기에 충분한 레지스터를 가지고 있지 않습니다.
- 이 코드는 Visual Studio 2010/2012 및 GCC 4.6에서 잘 작동하도록 테스트되었습니다.
ICC 11 (인텔 컴파일러 11)은 놀랍게도 잘 컴파일하는 데 문제가 있습니다. - 이는 FMA 이전 프로세서 용입니다. Intel Haswell 및 AMD Bulldozer 프로세서 (이상)에서 최대 FLOPS를 달성하려면 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.4GHz입니다. 이론적 인 SSE 피크는 4 플롭 * 4.4GHz = 17.6GFlops 입니다. 이 코드는 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.4GHz = 70.4GFlops입니다. 실제는 65.5GFlops 입니다.
한 단계 더 나아가 보겠습니다. 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.4GHz = 35.2GFlops 입니다. 실제는 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.4GHz = 140.8GFlops입니다. 실제는 138.2 GFlops 입니다.
이제 몇 가지 설명을 위해 :
성능에 중요한 부분은 분명히 내부 루프 내부의 48 개 명령입니다. 각각 12 개의 명령어로 구성된 4 개의 블록으로 나뉘어져 있음을 알 수 있습니다. 이 12 개의 명령어 블록 각각은 서로 완전히 독립적이며 실행하는 데 평균 6 사이클이 걸립니다.
따라서 발행부터 사용까지 12 개의 지침과 6 개의주기가 있습니다. 곱셈의 대기 시간은 5주기이므로 대기 시간 지연을 피하는 데 충분합니다.
데이터가 오버플로 / 언더 플로되지 않도록 정규화 단계가 필요합니다. 이것은 아무것도하지 않는 코드가 데이터의 크기를 천천히 증가 / 감소시키기 때문에 필요합니다.
따라서 모든 0을 사용하고 정규화 단계를 제거하면 실제로 이보다 더 잘할 수 있습니다. 그러나 전력 소비와 온도를 측정하기 위해 벤치 마크를 작성했기 때문에 플롭이 0이 아닌 "실제"데이터에 있는지 확인해야 했습니다. 실행 단위는 전력을 덜 사용하는 0에 대해 특수한 케이스 처리를 할 수 있기 때문입니다. 더 적은 열을 생성합니다.
더 많은 결과:
- Intel Core i7 920 @ 3.5GHz
- 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.5GHz = 14.0GFlops . 실제는 13.3 GFlops 입니다.
글 : 8
Seconds = 149.576
FP Ops = 7680000000000
FLOPs = 5.13452e+010
sum = 17.8122
이론적 SSE 피크 : 4 플롭 * 4 코어 * 3.5GHz = 56.0GFlops . 실제는 51.3 GFlops 입니다.
내 프로세서 온도는 다중 스레드 실행에서 76C를 기록했습니다! 이를 실행하는 경우 결과가 CPU 스로틀 링의 영향을받지 않는지 확인하십시오.
- 2 x Intel Xeon X5482 Harpertown @ 3.2GHz
- 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.2GHz = 12.8GFlops . 실제는 12.3 GFlops 입니다.
글 : 8
Seconds = 78.4733
FP Ops = 7680000000000
FLOPs = 9.78676e+10
sum = 17.8122
이론적 SSE 피크 : 4 플롭 * 8 코어 * 3.2GHz = 102.4GFlops . 실제는 97.9 GFlops 입니다.
인텔 아키텍처에는 사람들이 종종 잊어 버리는 지점이 있습니다. 디스패치 포트는 Int와 FP / SIMD간에 공유됩니다. 즉, 루프 논리가 부동 소수점 스트림에 거품을 생성하기 전에 일정량의 FP / SIMD 버스트 만 얻을 수 있습니다. Mystical은 풀린 루프에서 더 긴 스트라이드를 사용했기 때문에 코드에서 더 많은 플랍을 얻었습니다.
여기에서 Nehalem / Sandy Bridge 아키텍처를 살펴보면 http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 어떤 일이 발생하는지 매우 분명합니다.
반대로 INT 및 FP / SIMD 파이프에는 자체 스케줄러가있는 별도의 발급 포트가 있으므로 AMD (Bulldozer)에서 최고 성능에 도달하는 것이 더 쉽습니다.
테스트 할 프로세서가 없기 때문에 이것은 이론적입니다.
분기는 이론상 최고 성능을 유지하지 못하도록 확실히 막을 수 있습니다. 루프 풀기를 수동으로 수행하면 차이가 있습니까? 예를 들어 루프 반복 당 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;
}
2.4GHz Intel Core 2 Duo에서 Intels icc 버전 11.1을 사용하면
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 Gflops에 매우 가깝습니다.
편집하다:
죄송합니다. 어셈블리 코드를 살펴보면 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;
}
둘째, 벤치 마크 프로그램을 여러 번 실행하고 최상의 성능 만 사용해야합니다 . 현대 운영 체제에서는 많은 일이 병렬로 발생하고 CPU는 저주파 절전 모드에있을 수 있습니다. 프로그램을 반복적으로 실행하면 이상적인 경우에 더 가까운 결과를 얻을 수 있습니다.
'Program Tip' 카테고리의 다른 글
Node.js 배포 설정 / 구성 파일을 저장하는 방법은 무엇입니까? (0) | 2020.10.02 |
---|---|
MySQL 데이터베이스 / 테이블 / 열이 어떤 문자 집합인지 어떻게 알 수 있습니까? (0) | 2020.10.02 |
부트 스트래핑이란 무엇입니까? (0) | 2020.10.02 |
C # 인터페이스. (0) | 2020.10.02 |
수정 된 커밋을 원격 Git 저장소에 푸시하려면 어떻게해야합니까? (0) | 2020.10.02 |