Làm cách nào để đạt được tối đa lý thuyết 4 FLOP mỗi chu kỳ?


642

Làm thế nào có thể đạt được hiệu suất cao nhất về mặt lý thuyết của 4 thao tác điểm nổi (độ chính xác kép) trên mỗi chu kỳ trên CPU Intel x86-64 hiện đại?

Theo tôi hiểu, phải mất ba chu kỳ cho một SSE add và năm chu kỳ mulđể hoàn thành trên hầu hết các CPU Intel hiện đại (xem ví dụ 'Bảng hướng dẫn' của Agner Fog ). Do đường ống, người ta có thể nhận được thông lượng một addlần trong mỗi chu kỳ nếu thuật toán có ít nhất ba phép tính tổng độc lập. Vì điều đó đúng với addpdcác addsdphiên bản đóng gói cũng như các thanh ghi vô hướng và các thanh ghi SSE có thể chứa hai doublethông lượng có thể bằng hai flop mỗi chu kỳ.

Hơn nữa, dường như (mặc dù tôi chưa thấy bất kỳ tài liệu phù hợp nào về vấn đề này) addmulcó thể được thực thi song song với thông lượng tối đa về mặt lý thuyết là bốn flops mỗi chu kỳ.

Tuy nhiên, tôi không thể sao chép hiệu suất đó bằng chương trình C / C ++ đơn giản. Nỗ lực tốt nhất của tôi dẫn đến khoảng 2,7 flops / chu kỳ. Nếu bất cứ ai cũng có thể đóng góp một chương trình C / C ++ hoặc trình biên dịch chương trình biên dịch đơn giản thể hiện hiệu suất cao nhất sẽ được đánh giá cao.

Nỗ lực của tôi:

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

Tổng hợp với

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

tạo ra đầu ra sau trên Intel Core i5-750, 2,66 GHz.

addmul:  0.270 s, 3.707 Gflops, res=1.326463

Đó là, chỉ khoảng 1,4 flops mỗi chu kỳ. Nhìn vào mã trình biên dịch với g++ -S -O2 -march=native -masm=intel addmul.cppvòng lặp chính có vẻ tối ưu với tôi:

.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

Thay đổi các phiên bản vô hướng với các phiên bản đóng gói ( addpdmulpd) sẽ tăng gấp đôi số lượng flop mà không thay đổi thời gian thực hiện và vì vậy tôi chỉ nhận được 2,8 flop mỗi chu kỳ. Có một ví dụ đơn giản mà đạt được bốn flops mỗi chu kỳ?

Chương trình nhỏ xinh của Mysticial; đây là kết quả của tôi (chỉ chạy trong vài giây):

  • gcc -O2 -march=nocona: 5,6 Gflops trong số 10,66 Gflops (2,1 flops / chu kỳ)
  • cl /O2, openmp đã bị xóa: 10.1 Gflops trong số 10,66 Gflops (3,8 flops / chu kỳ)

Tất cả có vẻ hơi phức tạp, nhưng kết luận của tôi cho đến nay:

  • gcc -O2thay đổi thứ tự của các phép toán dấu phẩy động độc lập với mục đích xen kẽ addpdmulpdnếu có thể. Áp dụng tương tự cho gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona dường như giữ nguyên thứ tự của các phép toán dấu phẩy động như được định nghĩa trong nguồn C ++.

  • cl /O2, trình biên dịch 64 bit từ SDK cho Windows 7 sẽ tự động hủy vòng lặp và dường như thử và sắp xếp các hoạt động sao cho các nhóm ba addpdthay thế với ba mulpd(ít nhất là trên hệ thống của tôi và cho chương trình đơn giản của tôi) .

  • My Core i5 750 ( kiến trúc Nehalem ) không thích xen kẽ các add và mul và dường như không thể chạy song song cả hai hoạt động. Tuy nhiên, nếu được nhóm thành 3 thì nó đột nhiên hoạt động như ma thuật.

  • Các kiến ​​trúc khác (có thể là Sandy Bridge và các công trình khác) dường như có thể thực thi song song add / mul mà không gặp vấn đề gì nếu chúng thay thế trong mã lắp ráp.

  • Mặc dù khó thừa nhận, nhưng trên hệ thống của tôi cl /O2thực hiện công việc tốt hơn nhiều ở các hoạt động tối ưu hóa ở mức độ thấp cho hệ thống của tôi và đạt được hiệu suất cao nhất cho ví dụ C ++ nhỏ ở trên. Tôi đo được giữa 1,85-2,01 flops / chu kỳ (đã sử dụng đồng hồ () trong Windows không chính xác. Tôi đoán, cần sử dụng bộ hẹn giờ tốt hơn - cảm ơn Mackie Messer).

  • Cách tốt nhất tôi quản lý gcclà lặp lại thủ công hủy đăng ký và sắp xếp các phép cộng và phép nhân trong các nhóm ba. Với g++ -O2 -march=nocona addmul_unroll.cpp tôi nhận được tốt nhất 0.207s, 4.825 Gflopstương ứng với 1,8 flops / chu kỳ mà bây giờ tôi khá hài lòng.

Trong mã C ++, tôi đã thay thế forvòng lặp bằng

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

Và lắp ráp bây giờ trông giống như

.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
Dựa vào thời gian wallclock có lẽ là một phần của nguyên nhân. Giả sử bạn đang chạy cái này bên trong HĐH như Linux, bạn có thể tự do bỏ qua quy trình của mình bất cứ lúc nào. Đó là loại sự kiện bên ngoài có thể ảnh hưởng đến các phép đo hiệu suất của bạn.
tdenniston

Phiên bản GCC của bạn là gì? Nếu bạn đang sử dụng máy Mac mặc định, bạn sẽ gặp sự cố (đó là bản 4.2 cũ).
trận bán kết

2
Có chạy Linux nhưng không có tải trên hệ thống và việc lặp lại nhiều lần tạo ra sự khác biệt nhỏ (ví dụ: phạm vi 4.0-4.2 Gflops cho phiên bản vô hướng, nhưng bây giờ với -funroll-loops). Đã thử với gcc phiên bản 4.4.1 và 4.6.2, nhưng đầu ra asm có vẻ ổn?
dùng1059432

Bạn đã thử -O3cho gcc, cho phép -ftree-vectorize? Có lẽ kết hợp với -funroll-loopsmặc dù tôi không nếu điều đó thực sự cần thiết. Sau đó, việc so sánh có vẻ không công bằng nếu một trong các trình biên dịch thực hiện vector hóa / không kiểm soát, trong khi cái kia không phải vì nó không thể, nhưng vì nó không được nói quá.
Grizzly

4
@Grizzly -funroll-loopscó lẽ là một cái gì đó để thử. Nhưng tôi nghĩ -ftree-vectorizelà bên cạnh quan điểm. OP đang cố gắng duy trì 1 mul + 1 thêm hướng dẫn / chu kỳ. Các hướng dẫn có thể là vô hướng hoặc vectơ - không thành vấn đề vì độ trễ và thông lượng là như nhau. Vì vậy, nếu bạn có thể duy trì 2 / chu kỳ với SSE vô hướng, thì bạn có thể thay thế chúng bằng vector SSE và bạn sẽ đạt được 4 flops / chu kỳ. Trong câu trả lời của tôi, tôi đã làm điều đó từ SSE -> AVX. Tôi đã thay thế tất cả SSE bằng AVX - cùng độ trễ, cùng thông lượng, gấp đôi flops.
Bí ẩn

Câu trả lời:


517

Tôi đã thực hiện nhiệm vụ chính xác này trước đây. Nhưng nó chủ yếu để đo mức tiêu thụ năng lượng và nhiệt độ CPU. Đoạn mã sau (khá dài) đạt gần tối ưu trên Core i7 2600K của tôi.

Điều quan trọng cần lưu ý ở đây là số lượng lớn vòng lặp thủ công - không kiểm soát cũng như xen kẽ các bội số và thêm ...

Dự án đầy đủ có thể được tìm thấy trên GitHub của tôi: https://github.com/Mysticial/Flops

Cảnh báo:

Nếu bạn quyết định biên dịch và chạy nó, hãy chú ý đến nhiệt độ CPU của bạn !!!
Hãy chắc chắn rằng bạn không quá nóng. Và đảm bảo điều chỉnh CPU không ảnh hưởng đến kết quả của bạn!

Hơn nữa, tôi không chịu trách nhiệm cho bất kỳ thiệt hại nào có thể xảy ra do chạy mã này.

Ghi chú:

  • Mã này được tối ưu hóa cho x64. x86 không có đủ các thanh ghi để biên dịch tốt.
  • Mã này đã được thử nghiệm để hoạt động tốt trên Visual Studio 2010/2012 và GCC 4.6.
    ICC 11 (Intel Compiler 11) đáng ngạc nhiên đã gặp sự cố khi biên dịch nó tốt.
  • Đây là cho các bộ xử lý trước FMA. Để đạt được FLOPS cao nhất trên bộ xử lý Intel Haswell và AMD Bulldozer (và sau này), sẽ cần các hướng dẫn FMA (Fuse Multiply Add). Đây là vượt quá phạm vi của điểm chuẩn này.

#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");
}

Đầu ra (1 luồng, 10000000 lần lặp) - Được biên dịch với Visual Studio 2010 SP1 - x64 Phát hành:

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

Máy là Core i7 2600K @ 4,4 GHz. Đỉnh SSE lý thuyết là 4 flops * 4,4 GHz = 17,6 GFlops . Mã này đạt được 17,3 GFlops - không tệ.

Đầu ra (8 luồng, 10000000 lần lặp) - Được biên dịch với Visual Studio 2010 SP1 - x64 Phát hành:

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

Đỉnh SSE lý thuyết là 4 flops * 4 lõi * 4,4 GHz = 70,4 GFlops. Thực tế là 65,5 GFlops .


Hãy tiến lên một bước nữa. 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");
}

Đầu ra (1 luồng, 10000000 lần lặp) - Được biên dịch với Visual Studio 2010 SP1 - x64 Phát hành:

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

Đỉnh AVX lý thuyết là 8 flops * 4,4 GHz = 35,2 GFlops . Thực tế là 33,4 GF .

Đầu ra (8 luồng, 10000000 lần lặp) - Được biên dịch với Visual Studio 2010 SP1 - x64 Phát hành:

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

Đỉnh AVX lý thuyết là 8 flops * 4 lõi * 4,4 GHz = 140,8 GFlops. Thực tế là 138,2 GFlops .


Bây giờ cho một số giải thích:

Phần quan trọng về hiệu năng rõ ràng là 48 lệnh bên trong vòng lặp bên trong. Bạn sẽ nhận thấy rằng nó được chia thành 4 khối 12 hướng dẫn mỗi khối. Mỗi khối trong số 12 khối lệnh này hoàn toàn độc lập với nhau - và thực hiện trung bình 6 chu kỳ để thực hiện.

Vì vậy, có 12 hướng dẫn và 6 chu kỳ giữa các vấn đề sử dụng. Độ trễ của phép nhân là 5 chu kỳ, vì vậy nó chỉ đủ để tránh các quầy hàng có độ trễ.

Bước chuẩn hóa là cần thiết để giữ cho dữ liệu không bị tràn / tràn. Điều này là cần thiết vì mã không làm gì sẽ từ từ tăng / giảm độ lớn của dữ liệu.

Vì vậy, thực sự có thể làm tốt hơn thế này nếu bạn chỉ sử dụng tất cả các số không và thoát khỏi bước chuẩn hóa. Tuy nhiên, vì tôi đã viết điểm chuẩn để đo mức tiêu thụ năng lượng và nhiệt độ, tôi phải đảm bảo các flop là trên dữ liệu "thực", chứ không phải số không - vì các đơn vị thực thi có thể xử lý trường hợp đặc biệt cho các số không sử dụng ít năng lượng hơn và sản xuất ít nhiệt hơn.


Kết quả khác:

  • Intel Core i7 920 @ 3.5 GHz
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1 - Phát hành x64

Chủ đề: 1

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

Đỉnh SSE lý thuyết: 4 flops * 3,5 GHz = 14,0 GFlops . Thực tế là 13,3 GF .

Chủ đề: 8

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

Đỉnh SSE lý thuyết: 4 flops * 4 lõi * 3.5 GHz = 56.0 GFlops . Thực tế là 51,3 GF .

Temps bộ xử lý của tôi đạt 76C trên đường chạy đa luồng! Nếu bạn chạy những thứ này, hãy chắc chắn rằng kết quả không bị ảnh hưởng bởi điều tiết CPU.


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

Chủ đề: 1

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

Đỉnh SSE lý thuyết: 4 flops * 3,2 GHz = 12,8 GFlops . Thực tế là 12,3 GF .

Chủ đề: 8

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

Đỉnh SSE lý thuyết: 4 flops * 8 lõi * 3,2 GHz = 102,4 GFlops . Thực tế là 97,9 GFlops .


13
Kết quả của bạn rất ấn tượng. Tôi đã biên dịch mã của bạn với g ++ trên hệ thống cũ hơn nhưng không thu được kết quả tốt như sau: 100 nghìn lần lặp, 1.814s, 5.292 Gflops, sum=0.448883trong số 10,68 Gflops cao nhất hoặc chỉ thiếu 2.0 flops mỗi chu kỳ. Có vẻ add/ mulkhông được thực hiện song song. Khi tôi thay đổi mã của bạn và luôn thêm / nhân với cùng một thanh ghi, giả sử rC, nó đột nhiên đạt được gần như cực đại: 0.953s, 10.068 Gflops, sum=0hoặc 3,8 flops / chu kỳ. Rất lạ.
dùng1059432

11
Có, vì tôi không sử dụng lắp ráp nội tuyến, hiệu suất thực sự rất nhạy cảm với trình biên dịch. Mã tôi có ở đây đã được điều chỉnh cho VC2010. Và nếu tôi nhớ lại một cách chính xác, Trình biên dịch Intel cho kết quả tốt như vậy. Như bạn đã nhận thấy, bạn có thể phải điều chỉnh nó một chút để biên dịch tốt.
Bí ẩn

8
Tôi có thể xác nhận kết quả của bạn trên Windows 7 bằng cách sử dụng cl /O2(64-bit từ windows sdk) và thậm chí ví dụ của tôi chạy gần đến đỉnh điểm cho các hoạt động vô hướng (1.9 flops / chu kỳ) ở đó. Trình biên dịch vòng lặp unrolls và sắp xếp lại nhưng đó có thể không phải là lý do cần phải xem xét thêm một chút. Điều chỉnh không phải là vấn đề Tôi tốt với cpu của mình và giữ số lần lặp ở mức 100k. :)
dùng1059432

6
@Mysticial: Nó xuất hiện trên subreddit r / mã hóa ngày hôm nay.
greyfade

2
@haylem Nó hoặc tan chảy hoặc mất. Không bao giờ cả hai. Nếu có đủ làm mát, nó sẽ có được thời gian phát sóng. Nếu không, nó chỉ tan chảy. :)
Bí ẩn

33

Có một điểm trong kiến ​​trúc Intel mà mọi người thường quên, các cổng điều phối được chia sẻ giữa Int và FP / SIMD. Điều này có nghĩa là bạn sẽ chỉ nhận được một số lượng nhất định của FP / SIMD trước khi logic vòng lặp sẽ tạo ra các bong bóng trong luồng điểm nổi của bạn. Mystical nhận được nhiều flops hơn từ mã của mình, bởi vì anh ta đã sử dụng những bước dài hơn trong vòng lặp không được kiểm soát của mình.

Nếu bạn nhìn vào kiến ​​trúc Nehalem / Sandy Bridge tại đây http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 thì rõ ràng điều gì sẽ xảy ra.

Ngược lại, sẽ dễ dàng đạt được hiệu suất cao nhất trên AMD (Bulldozer) vì các ống INT và FP / SIMD có các cổng phát hành riêng biệt với bộ lập lịch riêng.

Đây chỉ là lý thuyết vì tôi không có bộ xử lý nào để kiểm tra.


2
Chỉ có ba hướng dẫn của vòng lặp overhead: inc, cmp, và jl. Tất cả những thứ này có thể đi đến cổng số 5 và không can thiệp vào véc tơ faddhoặc fmul. Tôi muốn nghi ngờ rằng bộ giải mã (đôi khi) đi vào đường. Nó cần phải duy trì giữa hai và ba hướng dẫn mỗi chu kỳ. Tôi không nhớ các giới hạn chính xác nhưng độ dài hướng dẫn, tiền tố và căn chỉnh đều có tác dụng.
Mackie Messer

cmpjlchắc chắn đi đến cổng 5, inckhông chắc lắm vì nó luôn đi cùng nhóm với 2 người khác. Nhưng bạn đã đúng, thật khó để nói nút thắt ở đâu và bộ giải mã cũng có thể là một phần của nó.
Patrick Schlüter

3
Tôi đã chơi xung quanh một chút với vòng lặp cơ bản: thứ tự của các hướng dẫn không thành vấn đề. Một số sắp xếp mất 13 chu kỳ thay vì 5 chu kỳ tối thiểu. Thời gian để xem các quầy sự kiện hiệu suất tôi đoán ...
Mackie Messer

16

Chi nhánh chắc chắn có thể giữ cho bạn khỏi duy trì hiệu suất lý thuyết đỉnh cao. Bạn có thấy sự khác biệt nếu bạn thực hiện một số thao tác hủy vòng lặp không? Ví dụ: nếu bạn đặt số lần lặp gấp 5 hoặc 10 lần cho mỗi vòng lặp:

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
Tôi có thể nhầm, nhưng tôi tin rằng g ++ với -O2 sẽ cố gắng tự động giải phóng vòng lặp (tôi nghĩ rằng nó sử dụng Thiết bị của Duff).
Thợ dệt

6
Vâng, cảm ơn thực sự nó cải thiện phần nào. Bây giờ tôi nhận được khoảng 4,1-4,3 Gflops, hoặc 1,55 flops mỗi chu kỳ. Và không, trong ví dụ này -O2 đã không lặp lại.
dùng1059432

1
Weaver là chính xác về unrolling vòng, tôi tin. Vì vậy, việc hủy đăng ký thủ công có lẽ là không cần thiết
jim mcnamara

5
Xem đầu ra lắp ráp ở trên, không có dấu hiệu của vòng lặp unrolling.
dùng1059432

14
Tự động hủy đăng ký cũng cải thiện trung bình 4.2 Gflops, nhưng yêu cầu -funroll-loopstùy chọn thậm chí không được bao gồm trong -O3. Xem g++ -c -Q -O2 --help=optimizers | grep unroll.
dùng1059432

7

Sử dụng Intels icc Phiên bản 11.1 trên Bộ đôi Intel Core 2 2.4GHz tôi nhận được

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 

Điều đó rất gần với 9,6 Gflops lý tưởng.

BIÊN TẬP:

Rất tiếc, nhìn vào mã lắp ráp, có vẻ như icc không chỉ véc tơ cho phép nhân mà còn kéo các phép cộng ra khỏi vòng lặp. Buộc một ngữ nghĩa fp chặt chẽ hơn, mã không còn được vector hóa:

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

EDIT2:

Như yêu cầu:

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

Vòng lặp bên trong của mã clang trông như thế này:

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

Cuối cùng, hai gợi ý: Thứ nhất, nếu bạn thích loại điểm chuẩn này, hãy cân nhắc sử dụng rdtschướng dẫn istead của gettimeofday(2). Nó chính xác hơn nhiều và cung cấp thời gian theo chu kỳ, thường là điều bạn quan tâm. Đối với gcc và bạn bè, bạn có thể định nghĩa nó như thế này:

#include <stdint.h>

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

Thứ hai, bạn nên chạy chương trình điểm chuẩn của mình nhiều lần và chỉ sử dụng hiệu suất tốt nhất . Trong các hệ điều hành hiện đại, nhiều điều xảy ra song song, cpu có thể ở chế độ tiết kiệm năng lượng tần số thấp, v.v. Chạy chương trình nhiều lần sẽ cho bạn kết quả gần với trường hợp lý tưởng hơn.


2
và sự tháo gỡ trông như thế nào?
Bahbar

1
Thật thú vị, đó là ít hơn 1 flop / chu kỳ. Trình biên dịch có trộn lẫn giữa addsdchúng và mulsdchúng theo nhóm như trong đầu ra lắp ráp của tôi không? Tôi cũng chỉ nhận được khoảng 1 flop / chu kỳ khi trình biên dịch trộn chúng (mà tôi không có -march=native). Hiệu suất thay đổi như thế nào nếu bạn thêm một dòng add=mul;ở đầu hàm addmul(...)?
dùng1059432

1
@ user1059432: Các hướng dẫn addsdsubsdthực sự được trộn lẫn trong phiên bản chính xác. Tôi cũng đã thử clang 3.0, nó không trộn hướng dẫn và nó rất gần với 2 flops / chu kỳ trên bộ đôi lõi 2. Khi tôi chạy cùng mã trên máy tính xách tay lõi i5 của mình, việc trộn mã không tạo ra sự khác biệt. Tôi nhận được khoảng 3 flops / chu kỳ trong cả hai trường hợp.
Mackie Messer

1
@ user1059432: Cuối cùng, tất cả là về việc lừa trình biên dịch tạo mã "có ý nghĩa" cho một điểm chuẩn tổng hợp. Điều này khó hơn so với cái nhìn đầu tiên. (tức là icc vượt quá điểm chuẩn của bạn) Nếu tất cả những gì bạn muốn là chạy một số mã với tốc độ 4 lần / chu kỳ, điều dễ nhất là viết một vòng lặp lắp ráp nhỏ. Ít đầu hơn nhiều. :-)
Mackie Messer

1
Ok, vậy là bạn đã đạt gần 2 flops / chu kỳ với mã lắp ráp tương tự như những gì tôi đã trích dẫn ở trên? Làm thế nào gần với 2? Tôi chỉ nhận được 1,4 vì vậy đó là đáng kể. Tôi không nghĩ rằng bạn nhận được 3 flops / chu kỳ trên máy tính xách tay của mình trừ khi trình biên dịch thực hiện tối ưu hóa như bạn đã thấy icctrước đây, bạn có thể kiểm tra lắp ráp không?
dùng1059432
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.