ฉันจะประสบความสำเร็จสูงสุดทางทฤษฎีของ 4 FLOPs ต่อรอบได้อย่างไร


642

ประสิทธิภาพสูงสุดในเชิงทฤษฎีของการดำเนินการจุดลอย 4 จุด (ความแม่นยำสองเท่า) ต่อรอบสามารถทำได้บนซีพียู x86-64 ที่ทันสมัยของ Intel?

เท่าที่ฉันเข้าใจมันใช้เวลาสามรอบสำหรับSSE addและห้ารอบเพื่อmulให้เสร็จสมบูรณ์บน CPU Intel ส่วนใหญ่ที่ทันสมัย ​​(ดูตัวอย่าง'Instruction Tables' ของ Agner Fog ) เนื่องจากการส่งไปป์ไลน์หนึ่งสามารถรับปริมาณงานหนึ่งaddต่อรอบหากอัลกอริทึมมีการสรุปอิสระอย่างน้อยสามครั้ง เนื่องจากเป็นจริงสำหรับแพ็กเกจaddpdรวมถึงaddsdรุ่นสเกลาร์และรีจิสเตอร์ SSE สามารถมีปริมาณงานสองdoubleรายการได้มากถึงสอง flops ต่อรอบ

นอกจากนี้ดูเหมือนว่า (แม้ว่าฉันจะไม่เห็นเอกสารที่ถูกต้องเกี่ยวกับเรื่องนี้) addและmulสามารถดำเนินการในแบบคู่ขนานโดยให้ทรูพุตสูงสุดตามทฤษฎีของสี่ flops ต่อรอบ

อย่างไรก็ตามฉันไม่สามารถจำลองประสิทธิภาพนั้นด้วยโปรแกรม C / C ++ อย่างง่าย ความพยายามที่ดีที่สุดของฉันส่งผลให้ประมาณ 2.7 flops / รอบ หากใครสามารถมีส่วนร่วม 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 flops ต่อรอบ ดูโค้ดแอสเซมเบลอร์ที่มี 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 ) จะเพิ่มจำนวน flop เป็นสองเท่าโดยไม่เปลี่ยนเวลาดำเนินการและดังนั้นฉันจะได้รับเพียง 2.8 flop ต่อรอบ มีตัวอย่างง่ายๆที่ประสบความสำเร็จสี่ flops ต่อรอบ?

โปรแกรมเล็ก ๆ ที่ดีโดย Mysticial; นี่คือผลลัพธ์ของฉัน (เรียกใช้เพียงไม่กี่วินาทีแม้ว่า):

  • gcc -O2 -march=nocona: 5.6 Gflops จาก 10.66 Gflops (2.1 flops / รอบ)
  • cl /O2, openmp ถูกนำออก: 10.1 Gflops จาก 10.66 Gflops (3.8 flops / รอบ)

ดูเหมือนว่าจะซับซ้อนเล็กน้อย แต่ข้อสรุปของฉัน:

  • gcc -O2เปลี่ยนลำดับของการดำเนินการจุดลอยตัวอิสระโดยมีจุดประสงค์ในการสลับ addpdและmulpdถ้าเป็นไปได้ gcc-4.6.2 -O2 -march=core2เช่นเดียวกับ

  • gcc -O2 -march=nocona ดูเหมือนว่าจะรักษาลำดับของการดำเนินการจุดลอยตัวตามที่กำหนดไว้ในแหล่ง C ++

  • cl /O2คอมไพเลอร์ 64 บิตจาก SDK สำหรับ Windows 7 ทำการวนลูปโดยอัตโนมัติและดูเหมือนว่าจะลองและจัดเรียงการดำเนินการเพื่อให้กลุ่มที่สามaddpdสลับกับสามmulpd(ดีอย่างน้อยในระบบของฉันและโปรแกรมง่าย ๆ ของฉัน) .

  • My Core i5 750 ( สถาปัตยกรรม Nehalem ) ไม่ชอบการเพิ่มและสลับระหว่างและดูเหมือนว่าจะไม่สามารถทำงานทั้งสองแบบขนาน อย่างไรก็ตามหากจัดกลุ่มเป็น 3 มันจะทำงานได้เหมือนเวทมนตร์

  • สถาปัตยกรรมอื่น ๆ (อาจจะเป็นSandy Bridgeและอื่น ๆ ) ดูเหมือนว่าจะสามารถเพิ่ม / mul ในแบบคู่ขนานโดยไม่มีปัญหาหากพวกเขาสลับกันในรหัสการประกอบ

  • แม้ว่าจะยากที่จะยอมรับ แต่ในระบบของฉันทำงานcl /O2ได้ดีขึ้นมากในการดำเนินการปรับแต่งในระดับต่ำให้กับระบบของฉันและได้รับประสิทธิภาพใกล้เคียงกับประสิทธิภาพสูงสุดสำหรับตัวอย่าง C ++ เล็กน้อยด้านบน ฉันวัดได้ระหว่าง 1.85-2.01 flops / cycle (เคยใช้ clock () ใน Windows ซึ่งไม่แม่นยำเท่านี้ฉันเดาว่าต้องใช้ตัวจับเวลาที่ดีกว่า - ขอบคุณ Mackie Messer)

  • สิ่งที่ดีที่สุดที่ฉันจัดการด้วยgccคือการวนซ้ำการเปิดและจัดเรียงการเพิ่มและการคูณในกลุ่มที่สามด้วยตนเอง เมื่อ g++ -O2 -march=nocona addmul_unroll.cpp ฉันได้ดีที่สุด0.207s, 4.825 Gflopsซึ่งตรงกับ 1.8 flops / รอบซึ่งตอนนี้ฉันมีความสุขมาก

ในรหัส 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 มันมีอิสระที่จะกำหนดกระบวนการของคุณได้ตลอดเวลา เหตุการณ์ภายนอกประเภทนั้นอาจส่งผลกระทบต่อการวัดประสิทธิภาพของคุณ
tdenniston

รุ่น GCC ของคุณคืออะไร หากคุณใช้เครื่อง Mac โดยใช้ค่าเริ่มต้นคุณจะพบปัญหา (เป็นรุ่น 4.2)
semisight

2
ใช่ใช้งาน Linux แต่ไม่มีโหลดบนระบบและทำซ้ำหลายครั้งสร้างความแตกต่างเล็กน้อย (เช่นช่วง 4.0-4.2 Gflops สำหรับรุ่นสเกลาร์ แต่ตอนนี้มี-funroll-loops) ลองกับ gcc รุ่น 4.4.1 และ 4.6.2 แต่เอาต์พุต asm ดูโอเคไหม?
user1059432

คุณลอง-O3gcc ซึ่งเปิดใช้งาน-ftree-vectorizeหรือไม่ อาจรวมกับ-funroll-loopsแม้ว่าฉันไม่ได้ถ้ามันจำเป็นจริงๆ หลังจากนั้นการเปรียบเทียบจะดูไม่ยุติธรรมเลยหากคอมไพเลอร์ตัวใดตัวหนึ่งทำการ vectorization / unrolling ในขณะที่อันอื่นไม่ได้เพราะมันทำไม่ได้ แต่เพราะมันบอกไม่เกินไป
Grizzly

4
@Grizzly -funroll-loopsอาจเป็นสิ่งที่ควรลอง แต่ฉันคิดว่า-ftree-vectorizeนอกเหนือจากจุด OP กำลังพยายามเพียงรักษา 1 mul + 1 เพิ่มคำแนะนำ / รอบ คำแนะนำสามารถเป็นเซนต์คิตส์และเนวิส - มันไม่สำคัญเนื่องจากความหน่วงและปริมาณงานเท่ากัน ดังนั้นถ้าคุณสามารถรักษา 2 / รอบด้วยเซนต์คิตส์และเนวิสแล้วคุณสามารถแทนที่ด้วยเวกเตอร์ SSE และคุณจะบรรลุ 4 flops / รอบ ในคำตอบของฉันฉันแค่ทำจาก SSE -> AVX ฉันแทนที่ SSE ทั้งหมดด้วย AVX - เวลาแฝงเดียวกัน, ปริมาณงานเท่ากัน, 2x ปริมาณสัญญาณ
ลึกลับ

คำตอบ:


517

ฉันเคยทำงานนี้มาก่อน แต่ส่วนใหญ่จะเป็นการวัดการใช้พลังงานและอุณหภูมิของ CPU รหัสต่อไปนี้ (ซึ่งค่อนข้างยาว) ได้ใกล้เคียงที่สุดกับ Core i7 2600K ของฉัน

สิ่งสำคัญที่ควรทราบที่นี่คือจำนวนมากของการวนซ้ำแบบแมนนวลที่ไม่ได้หมุนรวมถึงการเพิ่มจำนวนทวีคูณและเพิ่ม ...

โครงการเต็มสามารถพบได้ใน GitHub ของฉัน: https://github.com/Mysticial/Flops

คำเตือน:

หากคุณตัดสินใจที่จะรวบรวมและเรียกใช้สิ่งนี้ให้ใส่ใจกับอุณหภูมิ CPU ของคุณ !!!
ตรวจสอบให้แน่ใจว่าคุณไม่ได้ทำให้ร้อนมากเกินไป และตรวจสอบให้แน่ใจว่า CPU-throttling ไม่มีผลกับผลลัพธ์ของคุณ!

นอกจากนี้ฉันไม่รับผิดชอบต่อความเสียหายใด ๆ ที่อาจเกิดขึ้นจากการเรียกใช้รหัสนี้

หมายเหตุ:

  • รหัสนี้เหมาะสำหรับ x64 x86 มีการลงทะเบียนไม่เพียงพอที่จะรวบรวมสิ่งนี้ได้ดี
  • รหัสนี้ได้รับการทดสอบว่าทำงานได้ดีบน Visual Studio 2010/2012 และ GCC 4.6
    ICC 11 (Intel Compiler 11) มีปัญหาในการรวบรวมที่ดีอย่างน่าประหลาดใจ
  • สิ่งเหล่านี้ใช้สำหรับโปรเซสเซอร์ pre-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 Release:

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

ตัวเครื่องเป็น Core i7 2600K @ 4.4 GHz ทฤษฎี SSE สูงสุดคือ 4 * แตะ 4.4 GHz = 17.6 GFLOPS รหัสนี้บรรลุ17.3 GFlops - ไม่เลว

ผลลัพธ์ (8 เธรด, วนซ้ำ 10,000,000) - คอมไพล์ด้วย Visual Studio 2010 SP1 - x64 Release:

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

SSE เชิงทฤษฎีสูงสุดคือ 4 flops * 4 cores * 4.4 GHz = 70.4 GFlops ที่เกิดขึ้นจริงเป็น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 Release:

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

ยอด AVX ทฤษฎีคือ 8 flops * 4.4 GHz = 35.2 GFLOPS ที่เกิดขึ้นจริงเป็น33.4 GFLOPS

ผลลัพธ์ (8 เธรด, วนซ้ำ 10,000,000) - คอมไพล์ด้วย Visual Studio 2010 SP1 - x64 Release:

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

สูงสุดตามทฤษฎี AVX คือ 8 flops * 4 แกน * 4.4 GHz = 140.8 GFlops ที่เกิดขึ้นจริงเป็น138.2 GFLOPS


ตอนนี้สำหรับคำอธิบายบางอย่าง:

ส่วนที่มีประสิทธิภาพที่สำคัญคือ 48 คำแนะนำภายในวงด้านใน คุณจะสังเกตเห็นว่ามันแบ่งออกเป็น 4 บล็อกจาก 12 คำแนะนำในแต่ละ บล็อกคำสั่งทั้ง 12 นี้แต่ละบล็อกนั้นมีความเป็นอิสระจากกัน - และใช้เวลาเฉลี่ย 6 รอบในการดำเนินการ

มี 12 คำแนะนำและ 6 รอบระหว่างการใช้งาน เวลาแฝงของการคูณคือ 5 รอบดังนั้นจึงเพียงพอที่จะหลีกเลี่ยงแผงลอย

ขั้นตอนการทำให้เป็นมาตรฐานเป็นสิ่งจำเป็นเพื่อป้องกันไม่ให้ข้อมูลสูง / ต่ำเกินไป สิ่งนี้จำเป็นเนื่องจากโค้ดที่ไม่ต้องทำอะไรจะเพิ่ม / ลดขนาดของข้อมูลอย่างช้าๆ

ดังนั้นจึงเป็นไปได้ที่จะทำได้ดีกว่านี้หากคุณใช้ศูนย์ทั้งหมดและกำจัดขั้นตอนการทำให้เป็นมาตรฐาน อย่างไรก็ตามเนื่องจากฉันเขียนมาตรฐานเพื่อวัดการใช้พลังงานและอุณหภูมิฉันต้องตรวจสอบให้แน่ใจว่า flops อยู่ในข้อมูล "ของจริง" แทนที่จะเป็นศูนย์เนื่องจากหน่วยปฏิบัติการอาจมีการจัดการกรณีพิเศษสำหรับศูนย์ที่ใช้พลังงานน้อยกว่า และผลิตความร้อนน้อยลง


ผลลัพธ์เพิ่มเติม:

  • Intel Core i7 920 @ 3.5 GHz
  • 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 flops * 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 flops * 4 แกน * 3.5 GHz = 56.0 GFLOPS ที่เกิดขึ้นจริงเป็น51.3 GFLOPS

temps โปรเซสเซอร์ของฉันกด 76C ในการทำงานแบบมัลติเธรด! หากคุณดำเนินการเหล่านี้ตรวจสอบให้แน่ใจว่าผลลัพธ์ไม่ได้รับผลกระทบจากการควบคุมปริมาณ CPU


  • 2 x Intel Xeon X5482 Harpertown ที่ 3.2 GHz
  • 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 flops * 3.2 GHz = 12.8 GFLOPS ที่เกิดขึ้นจริงเป็น12.3 GFLOPS

หัวข้อ: 8

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

ทฤษฎี SSE พี: 4 flops * * * * * * * * 8 แกน 3.2 GHz = 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 flops / รอบ ที่แปลกมาก.
user1059432

11
ใช่เนื่องจากฉันไม่ได้ใช้ชุดประกอบแบบอินไลน์ประสิทธิภาพจึงมีความละเอียดอ่อนมากต่อคอมไพเลอร์ รหัสที่ฉันมีที่นี่ได้รับการปรับสำหรับ VC2010 และถ้าฉันจำได้ถูกต้อง Intel Compiler ก็ให้ผลลัพธ์ที่ดีเช่นกัน ดังที่คุณสังเกตเห็นคุณอาจต้องปรับแต่งเล็กน้อยเพื่อให้สามารถคอมไพล์ได้ดี
Mysticial

8
ฉันสามารถยืนยันผลลัพธ์ของคุณบน Windows 7 โดยใช้cl /O2(64- บิตจาก windows sdk) และแม้กระทั่งตัวอย่างของฉันก็ทำงานได้ใกล้เคียงกับการดำเนินงานสเกลาร์ (1.9 flops / รอบ) ที่นั่น คอมไพเลอร์ loop-unrolls และ reorders แต่นั่นอาจไม่ใช่เหตุผลที่จำเป็นต้องพิจารณาเพิ่มเติมอีกเล็กน้อย การควบคุมปริมาณไม่ได้เป็นปัญหาฉันดีกับซีพียูของฉันและทำซ้ำที่ 100k :)
user1059432


2
@Haylem มันละลายหรือถอดออก ไม่เคยทั้งคู่ หากมีความเย็นเพียงพอก็จะได้เวลาออกอากาศ มิฉะนั้นมันจะละลาย :)
Mysticial

33

มีจุดหนึ่งในสถาปัตยกรรม Intel ที่ผู้คนมักจะลืมพอร์ตการแจกจ่ายจะถูกแชร์ระหว่าง Int และ FP / SIMD ซึ่งหมายความว่าคุณจะได้รับการระเบิด FP / SIMD จำนวนหนึ่งเท่านั้นก่อนที่ตรรกะการวนรอบจะสร้างฟองอากาศในสตรีมจุดลอยของคุณ ลึกลับได้หลุดพ้นจากรหัสของเขามากขึ้นเพราะเขาใช้ความก้าวหน้าอย่างต่อเนื่องในการวนซ้ำที่ไม่ได้ควบคุม

ถ้าคุณดูสถาปัตยกรรม Nehalem / Sandy Bridge ที่นี่ http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 มันค่อนข้างชัดเจนว่าเกิดอะไรขึ้น

ในทางตรงกันข้ามมันควรจะง่ายกว่าในการเข้าถึงประสิทธิภาพสูงสุดบน AMD (Bulldozer) เนื่องจาก INT และ FP / SIMD ไปป์มีพอร์ตปัญหาแยกต่างหากด้วยตัวกำหนดตารางเวลาของตนเอง

นี่เป็นเพียงทฤษฎีเท่านั้นที่ฉันไม่มีโปรเซสเซอร์เหล่านี้ในการทดสอบ


2
มีเพียงสามคำแนะนำของวงค่าใช้จ่ายคือinc, และcmp jlทั้งหมดเหล่านี้สามารถไปที่พอร์ต # 5 และไม่ยุ่งเกี่ยวกับอย่างใดอย่างหนึ่ง vectorized หรือfadd fmulฉันอยากจะสงสัยว่าตัวถอดรหัส (บางครั้ง) เข้าทางแล้ว มันต้องการที่จะรักษาระหว่างสองถึงสามคำแนะนำต่อรอบ ฉันจำข้อ จำกัด ที่แน่นอนไม่ได้ แต่ความยาวของคำสั่งคำนำหน้าและการจัดแนวทั้งหมดมาสู่การเล่น
Mackie Messer

cmpและjlไปที่พอร์ต 5 อย่างแน่นอนincไม่แน่ใจว่าจะมาพร้อมกับคนอื่น ๆ 2 คนเสมอ แต่คุณพูดถูกมันยากที่จะบอกว่าคอขวดอยู่ที่ไหนและตัวถอดรหัสสามารถเป็นส่วนหนึ่งของมันได้
Patrick Schlüter

3
ฉันเล่นกับวงพื้นฐานเล็กน้อย: การเรียงลำดับของคำแนะนำสำคัญ การเตรียมการบางอย่างใช้เวลา 13 รอบแทนขั้นต่ำ 5 รอบ ได้เวลาดูเคาน์เตอร์กิจกรรมการแสดงฉันเดาว่า ...
Mackie Messer

16

สาขาสามารถป้องกันไม่ให้คุณรักษาประสิทธิภาพทางทฤษฎีสูงสุด คุณเห็นความแตกต่างหรือไม่ถ้าคุณทำการวนซ้ำด้วยตนเอง ตัวอย่างเช่นถ้าคุณใส่ 5 หรือ 10 เท่าของ ops ต่อลูปซ้ำ:

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
ฉันอาจเข้าใจผิด แต่ฉันเชื่อว่า g ++ ด้วย -O2 จะพยายามคลายลูปโดยอัตโนมัติ (ฉันคิดว่ามันใช้อุปกรณ์ของ Duff)
ประกอบ

6
ใช่ขอบคุณแน่นอนมันช่วยปรับปรุงบ้าง ตอนนี้ฉันได้รับ 4.1-4.3 Gflops หรือ 1.55 flops ต่อรอบ และไม่ในตัวอย่างนี้ -O2 ไม่ได้วนซ้ำเปิด
user1059432

1
ผู้ประกอบถูกต้องเกี่ยวกับการคลี่คลายม้วนผมเชื่อว่า ดังนั้นการ
ยกเลิกการเปิดใช้

5
ดูเอาต์พุตชุดประกอบด้านบนไม่มีสัญญาณของการวนลูป
user1059432

14
คลี่อัตโนมัติยังช่วยเพิ่มเฉลี่ย 4.2 GFLOPS แต่ต้องมีตัวเลือกที่จะไม่รวมอยู่แม้ใน-funroll-loops ดู-O3 g++ -c -Q -O2 --help=optimizers | grep unroll
user1059432

7

การใช้ Intels icc เวอร์ชั่น 11.1 บน 2.4GHz 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 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 istead 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's และmulsd' หรือพวกมันเป็นกลุ่มเช่นเดียวกับในแอสเซมบลีเอาท์พุทของฉันหรือไม่? ฉันยังได้รับเพียง 1 flop / รอบเมื่อคอมไพเลอร์ผสมพวกเขา (ซึ่งฉันได้โดยไม่ต้อง-march=native) อย่างไรการดำเนินงานเปลี่ยนแปลงถ้าคุณเพิ่มบรรทัดadd=mul;ที่จุดเริ่มต้นของการทำงานaddmul(...)?
user1059432

1
@ user1059432: คำแนะนำaddsdและsubsdคำสั่งผสมกันแน่นอน ฉันลองเสียงดังกราว 3.0 ด้วยเช่นกันมันไม่ได้ผสมคำแนะนำและมันใกล้เคียงกับ 2 flops / รอบบน core 2 duo เมื่อฉันเรียกใช้รหัสเดียวกันบนแล็ปท็อปของฉัน core i5 การผสมรหัสทำให้ไม่แตกต่างกัน ฉันได้รับประมาณ 3 flops / cycle ในทั้งสองกรณี
Mackie Messer

1
@ user1059432: ในที่สุดมันเป็นเรื่องเกี่ยวกับการหลอกให้คอมไพเลอร์ในการสร้างรหัส "ความหมาย" สำหรับมาตรฐานการสังเคราะห์ มันยากกว่าที่คิดไว้ตั้งแต่แรก (เช่น icc outsmarts มาตรฐานของคุณ) หากสิ่งที่คุณต้องการคือการเรียกใช้รหัสบางอย่างที่ 4 flops / รอบสิ่งที่ง่ายที่สุดคือการเขียนห่วงประกอบขนาดเล็ก ปวดหัวน้อยกว่ามาก :-)
Mackie Messer

1
ตกลงคุณจะได้ใกล้เคียงกับ 2 flops / cycle โดยมีรหัสแอสเซมบลีที่คล้ายกับที่ฉันยกมาข้างต้น? ใกล้กับ 2 เท่าไหร่? ฉันได้รับ 1.4 เท่านั้นนั่นสำคัญ ฉันไม่คิดว่าคุณจะได้รับ 3 flops / รอบบนแล็ปท็อปของคุณเว้นแต่ว่าคอมไพเลอร์จะเพิ่มประสิทธิภาพตามที่คุณเคยเห็นiccมาก่อนคุณสามารถตรวจสอบชุดประกอบได้หรือไม่
user1059432
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.