사이클 당 이론적으로 최대 4 개의 FLOP를 달성하려면 어떻게해야합니까?


642

최신 x86-64 Intel CPU에서 사이클 당 4 개의 부동 소수점 연산 (이중 정밀도)의 이론적 최고 성능을 어떻게 달성 할 수 있습니까?

내가 아는 한 SSE 에는 3주기가 걸리고 대부분의 최신 인텔 CPU에서 완료하려면 add5주기가 필요합니다 mul(예 : Agner Fog의 'Instruction Tables'참조 ). add알고리즘에 최소 3 개의 독립적 인 합계가있는 경우 파이프 라이닝으로 인해 사이클 당 하나의 처리량을 얻을 수 있습니다 . addpd스칼라 addsd버전 및 SSE 레지스터 뿐만 아니라 팩 형도 마찬가지이므로 double처리량은 사이클 당 2 개의 플롭이 될 수 있습니다.

(나는 여기에 대한 적절한 문서를 본 적이 없다하더라도) 또한,이 보인다 add's와 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 GHz에서 다음 출력을 생성합니다.

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

압축 버전 ( addpdmulpd)으로 스칼라 버전을 변경하면 실행 시간을 변경하지 않고 플롭 수가 두 배가되므로 사이클 당 2.8 플롭이 부족합니다. 사이클 당 4 회 플롭을 달성하는 간단한 예가 있습니까?

Mysticial의 멋진 작은 프로그램; 여기 내 결과가 있습니다 (단 몇 초 동안 만 실행).

  • gcc -O2 -march=nocona: 10.66 Gflops 중 5.6 Gflops (2.1 flops / cycle)
  • 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(적어도 내 시스템과 간단한 프로그램에서는) .

  • My Core i5 750 ( Nehalem architecture )은 add와 mul을 교대로 좋아하지 않으며 두 작업을 동시에 실행할 수 없습니다. 그러나 3으로 그룹화하면 갑자기 마술처럼 작동합니다.

  • 다른 아키텍처 (아마도 Sandy Bridge 및 기타 아키텍처 )는 어셈블리 코드에서 번갈아 가며 문제없이 add / mul을 병렬로 실행할 수있는 것으로 보입니다.

  • 인정하기는 어렵지만 내 시스템 cl /O2에서는 시스템의 저수준 최적화 작업에서 훨씬 더 나은 작업을 수행하며 위의 작은 C ++ 예제에서 최대 성능에 가깝습니다. 1.85-2.01 flops / cycle 사이에서 측정했습니다 (Windows에서는 clock ()을 사용하지 않았습니다. 정확하지 않습니다. Mackie Messer 덕분에 더 나은 타이머를 사용해야합니다).

  • 내가 관리 한 최선 gcc은 수동으로 언롤을 반복하고 덧셈과 곱셈을 세 그룹으로 정렬하는 것입니다. 으로 g++ -O2 -march=nocona addmul_unroll.cpp 내가 최선을 얻을 0.207s, 4.825 Gflops1.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
...

15
벽시계 시간에 의존하는 것이 원인의 일부일 수 있습니다. Linux와 같은 OS 내에서 이것을 실행한다고 가정하면 언제든지 프로세스 일정을 자유롭게 조정할 수 있습니다. 이러한 종류의 외부 이벤트는 성능 측정에 영향을 줄 수 있습니다.
tdenniston

GCC 버전은 무엇입니까? 기본값을 사용하여 Mac을 사용하는 경우 문제가 발생합니다 (이전 4.2).
semisight

2
예. Linux를 실행하지만 시스템에로드가없고 여러 번 반복해도 차이가 거의 없습니다 (예 : 스칼라 버전의 경우 4.0-4.2Gflops 범위이지만 이제는 -funroll-loops). gcc 버전 4.4.1 및 4.6.2로 시도했지만 asm 출력이 정상입니까?
user1059432

-O3gcc 를 사용해 보았습니까 -ftree-vectorize? 어쩌면 -funroll-loops내가 정말로 필요하지 않은 경우 와 결합 할 수도 있습니다. 결국 컴파일러 중 하나가 벡터화 / 언 롤링을 수행하는 경우 비교는 다소 불공평 한 반면, 다른 하나는 그렇게 할 수 없기 때문이 아니라 그렇게 알려지지 않았기 때문에 비교할 수 없습니다.
그리즐리

4
@Grizzly -funroll-loops는 아마도 시도해야 할 것입니다. 그러나 나는 -ftree-vectorize요점 외에 있다고 생각 합니다. OP는 1 mul + 1 add 명령 / 사이클을 유지하려고합니다. 명령어는 스칼라 또는 벡터 일 수 있습니다. 지연 시간과 처리량이 동일하므로 중요하지 않습니다. 따라서 스칼라 SSE로 2 / 사이클을 유지할 수 있다면 벡터 SSE로 대체 할 수 있으며 사이클 / 사이클 4 개를 달성 할 수 있습니다. 내 대답에서 나는 SSE-> AVX에서 그 일을했습니다. 모든 SSE를 AVX로 교체했습니다. 동일한 대기 시간, 동일한 처리량, 2 배의 플롭입니다.
신비주의

답변:


517

나는 전에이 정확한 작업을 수행했습니다. 그러나 주로 전력 소비와 CPU 온도를 측정하는 것이 었습니다. 다음 코드는 상당히 길지만 Core i7 2600K에서 최적에 가깝습니다.

여기서 주목할 점은 대량의 수동 루프 언 롤링과 곱셈 및 추가 인터리빙입니다 ...

전체 프로젝트는 내 GitHub에서 찾을 수 있습니다 : https://github.com/Mysticial/Flops

경고:

이것을 컴파일하고 실행하기로 결정했다면, CPU 온도에주의하십시오 !!!
과열시키지 마십시오. 그리고 CPU 조절이 결과에 영향을 미치지 않도록하십시오!

또한이 코드 실행으로 인해 발생할 수있는 피해에 대해서는 책임을지지 않습니다.

노트:

  • 이 코드는 x64에 최적화되어 있습니다. x86에는 컴파일하기에 충분한 레지스터가 없습니다.
  • 이 코드는 Visual Studio 2010/2012 및 GCC 4.6에서 잘 작동하도록 테스트되었습니다.
    ICC 11 (Intel Compiler 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

머신은 4.4GHz에서 Core i7 2600K입니다. 이론적 SSE 피크는 4 플롭 * 4.4 GHz = 17.6 GFlops 입니다. 이 코드는 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.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 GHz = 35.2 GFlops 입니다. 실제는 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 GHz = 140.8 GFlops입니다. 실제는 138.2 GFlops 입니다.


이제 몇 가지 설명이 필요합니다.

성능 결정적인 부분은 분명히 내부 루프 내부의 48 개 명령입니다. 각각 12 개의 명령어로 구성된 4 개의 블록으로 나뉘어져 있습니다. 이 12 개의 명령어 블록 각각은 서로 완전히 독립적이며 실행하는 데 평균 6주기가 걸립니다.

따라서 사용 간 문제에는 12 개의 지침과 6 개의주기가 있습니다. 곱셈의 대기 시간은 5주기이므로 대기 시간이 멈추지 않도록 충분합니다.

정규화 단계는 데이터가 오버 / 언더 플로우되지 않도록 유지하는 데 필요합니다. 아무것도하지 않는 코드는 데이터의 크기를 천천히 증가 / 감소시키기 때문에 이것이 필요하다.

따라서 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.5 GHz의 퍼 = 14.0을 GFLOPS . 실제는 13.3 GFlops 입니다.

스레드 : 8

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

이론적 SSE 피크 : 4 플롭 * 4 코어 * 3.5 GHz = 56.0 GFlops . 실제는 51.3 GFlops 입니다.

멀티 스레드 실행에서 프로세서 온도가 76C에 도달했습니다! 이를 실행하면 결과가 CPU 조절의 영향을받지 않는지 확인하십시오.


  • Intel Xeon X5482 Harpertown @ 3.2GHz 2 개
  • 우분투 리눅스 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 GHz = 12.8 GFlops . 실제는 12.3 GFlops 입니다.

스레드 : 8

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

이론적 SSE 피크 8 개 코어가 3.2 기가 헤르쯔 = * 4 퍼 102.4 GFLOPS를 . 실제는 97.9 GFlops 입니다.


13
결과는 매우 인상적입니다. 나는 이전 시스템에서 g ++로 코드를 컴파일했지만 좋은 결과를 얻지 못했습니다 .100k 반복, 1.814s, 5.292 Gflops, sum=0.448883피크 10.68 Gflops 또는 사이클 당 2.0 flops 부족. add/ mul는 병렬로 실행되지 않는 것 같습니다 . 코드를 변경하고 항상 동일한 레지스터를 사용하여 추가 / 곱하면 rC, 거의 거의 최대 피크를 얻습니다 : 0.953s, 10.068 Gflops, sum=0또는 3.8 플롭 / 사이클. 아주 이상한.
user1059432

11
예, 인라인 어셈블리를 사용하지 않기 때문에 성능은 실제로 컴파일러에 매우 민감 합니다. 여기에있는 코드는 VC2010에 맞게 조정되었습니다. 그리고 올바르게 기억한다면 인텔 컴파일러는 좋은 결과를 제공합니다. 알다시피, 컴파일이 잘되도록 약간 조정해야 할 수도 있습니다.
Mysticial

8
cl /O2(Windows SDK에서 64 비트)를 사용하여 Windows 7에서 결과를 확인할 수 있으며 내 예제조차도 스칼라 작업 (1.9 플롭 / 사이클)에 대해 최고에 가깝게 실행됩니다. 컴파일러는 루프를 언 롤링하고 재정렬하지만, 이것에 대해 좀 더 자세히 살펴볼 필요는 없습니다. 스로틀 링 문제 없음 CPU에 익숙하고 반복을 100k로 유지하십시오. :)
user1059432

6
@Mysticial : 그것은 오늘 r / coding subreddit에 나타났습니다 .
greyfade

2
@haylem 녹거나 벗어납니다. 둘 다 냉각이 충분하면 방송 시간이 생깁니다. 그렇지 않으면 그냥 녹아 버립니다. :)
Mysticial

33

인텔 아키텍처에는 사람들이 종종 잊어 버리는 점이 있습니다. 디스패치 포트는 Int와 FP / SIMD 사이에 공유됩니다. 이는 루프 로직이 부동 소수점 스트림에 버블을 생성하기 전에 일정량의 FP / SIMD 버스트 만 얻을 수 있음을 의미합니다. Mystical은 언롤 된 루프에서 더 긴 보폭을 사용했기 때문에 코드에서 더 많은 플롭을 얻었습니다.

여기 Nehalem / Sandy Bridge 아키텍처 ( http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6)를 보면 어떤 일이 일어나는지 분명합니다.

반면 INT 및 FP / SIMD 파이프에는 자체 스케줄러가있는 별도의 문제 포트가 있으므로 AMD (Bulldozer)에서 최대 성능에 도달하기가 더 쉬워야합니다.

테스트 할 프로세서가 없으므로 이론적입니다.


2
이 루프 오버 헤드의 세 가지 방법은 다음과 같습니다 inc, cmp그리고 jl. 이들 모두는 포트 # 5로 이동하여 벡터화 fadd또는을 방해하지 않습니다 fmul. 오히려 디코더 (때로는)가 방해가되는 것으로 의심합니다. 사이클 당 2 ~ 3 개의 명령을 유지해야합니다. 정확한 제한은 기억 나지 않지만 명령 길이, 접두사 및 정렬이 모두 작동합니다.
Mackie Messer

cmp그리고 jl확실히, 포트 5에 가서 inc는이 다른 사람들과 그룹에 항상 제공되지 확신한다. 그러나 당신은 옳습니다. 병목 현상의 위치를 ​​알기가 어렵고 디코더도 그 부분이 될 수 있습니다.
Patrick Schlüter

3
나는 기본 루프로 조금 놀았습니다. 명령의 순서는 중요합니다. 일부 배열은 최소 5 사이클 대신 13 사이클을 사용합니다. 내가 생각하는 성능 이벤트 카운터를 볼 시간입니다 ...
Mackie Messer

16

브랜치는 최고의 이론적 성능을 유지하지 못하게합니다. 루프 언 롤링을 수동으로 수행하면 차이가 있습니까? 예를 들어 루프 반복마다 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;
   }

4
나는 잘못 생각할 수도 있지만 -O2가있는 g ++은 자동으로 루프를 풀려고 시도합니다 (더프 장치를 사용한다고 생각합니다).
Weaver

6
예, 실제로 다소 개선되었습니다. 이제 약 4.1-4.3 Gflops 또는 사이클 당 1.55 flops를 얻습니다. 그리고이 예에서 -O2는 롤 풀기를 반복하지 않았습니다.
user1059432

1
위버는 루프 언 롤링에 대해 정확하다고 생각합니다. 그래서 수동으로 줄이기 아마 필요가 없습니다
짐 맥나마라는

5
위의 어셈블리 출력을 참조하십시오. 루프 언 롤링 징후는 없습니다.
user1059432

14
자동 언 롤링도 평균 4.2Gflops로 향상되지만에 -funroll-loops포함되지 않은 옵션이 필요합니다 -O3. 참조하십시오 g++ -c -Q -O2 --help=optimizers | grep unroll.
user1059432

7

2.4GHz Intel Core 2 Duo에서 Intel 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는 저주파수 절전 모드 등일 수 있습니다. 프로그램을 반복적으로 실행하면 이상적인 경우에 더 가까운 결과를 얻을 수 있습니다.


2
그리고 분해는 어떻게 생겼습니까?
Bahbar

1
흥미롭게도, 그것은 1 플롭 / 사이클보다 적습니다. 컴파일러는 혼합 하는가 addsd'들과 mulsd의 또는 내 조립 출력으로 그룹에있는? 또한 컴파일러가 믹스 할 때 약 1 플롭 / 사이클을 얻습니다 (내가없는 -march=native). add=mul;함수 시작 부분에 줄을 추가하면 성능이 어떻게 달라 addmul(...)집니까?
user1059432

1
@ user1059432 : addsdsubsd지침은 실제로 정확한 버전으로 혼합되어 있습니다. clang 3.0도 사용해 보았습니다. 명령을 혼합하지 않으며 코어 2 듀오에서 사이클 / 사이클 2에 매우 가깝습니다. 랩톱 코어 i5에서 동일한 코드를 실행할 때 코드를 혼합해도 아무런 차이가 없습니다. 어느 경우 에나 약 3 회의 플롭 / 사이클을 얻습니다.
Mackie Messer

1
@ user1059432 : 결국은 합성 벤치 마크를위한 "의미있는"코드를 생성하도록 컴파일러를 속이는 것입니다. 처음 보았을 때보 다 어렵습니다. (즉, icc는 벤치 마크를 능가합니다.) 원하는 코드를 4 플롭 / 사이클로 실행하는 것이 가장 쉬운 방법은 작은 어셈블리 루프를 작성하는 것입니다. 훨씬 덜 headake. :-)
Mackie Messer

1
좋아, 그래서 당신은 위에서 인용 한 것과 비슷한 어셈블리 코드를 사용하여 사이클 / 사이클에 가깝습니다. 2에 얼마나 가깝습니까? 나는 1.4 만 얻었으므로 중요합니다. 컴파일러가 icc이전에 본 것처럼 최적화를 수행하지 않는 한 랩톱에서 사이클 3 회를 얻는다고 생각하지 않습니다 . 어셈블리를 다시 확인할 수 있습니까?
user1059432
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.