是否有一个真正有效的例子,展示了x86_64上ILP(指令级并行)的好处?

时间:2015-01-02 20:16:05

标签: c++ performance optimization assembly x86

由于已知CPU是管道,如果命令序列彼此独立,它的工作效率最高 - 这称为ILP(指令级并行):http://en.wikipedia.org/wiki/Instruction-level_parallelism

但是有没有一个真正有效的例子,它显示了ILP的好处,至少是同步的例子,对于CPU x86_64(但对于cmp / jne的相同数量< /强>)?

我将编写以下示例 - 添加数组的所有元素,但它没有显示ILP的任何优点:http://ideone.com/fork/poWfsm

  • 依序:
        for(i = 0; i < arr_size; i += 8) {
            result += arr[i+0] + arr[i+1] + 
                    arr[i+2] + arr[i+3] + 
                    arr[i+4] + arr[i+5] +
                    arr[i+6] + arr[i+7];
        }
  • ILP:
        register unsigned int v0, v1, v2, v3;
        v0 = v1 = v2 = v3 = 0;
        for(i = 0; i < arr_size; i += 8) {              
            v0 += arr[i+0] + arr[i+1];
            v1 += arr[i+2] + arr[i+3];
            v2 += arr[i+4] + arr[i+5];
            v3 += arr[i+6] + arr[i+7];
        }
        result = v0+v1+v2+v3;

结果:

  

seq:0.100000 sec,res:1000000000,ipl:0.110000 sec,更快 0.909091   X,res:1000000000

     

seq:0.100000 sec,res:1000000000,ipl:0.100000 sec,更快 1.000000   X,res:1000000000

     

seq:0.100000 sec,res:1000000000,ipl:0.110000 sec,更快 0.909091   X,res:1000000000

     

seq:0.100000 sec,res:1000000000,ipl:0.100000 sec,更快 1.000000   X,res:1000000000

     

seq:0.110000秒,res:1000000000,ipl:0.110000秒,更快 1.000000   X,res:1000000000

     

seq:0.100000 sec,res:1000000000,ipl:0.110000 sec,更快 0.909091   X,res:1000000000

     

seq:0.100000 sec,res:1000000000,ipl:0.110000 sec,更快 0.909091   X,res:1000000000

     

seq:0.110000 sec,res:1000000000,ipl:0.100000 sec,更快 1.100000   X,res:1000000000

     

seq:0.110000 sec,res:1000000000,ipl:0.100000 sec,更快 1.100000   X,res:1000000000

     

seq:0.110000秒,res:1000000000,ipl:0.120000秒,更快 0.916667   X,res:1000000000

     

更快的AVG: 0.975303

ILP甚至比Sequential慢一点。

C代码:http://ideone.com/fork/poWfsm

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    // create and init array
    const size_t arr_size = 100000000;
    unsigned int *arr = (unsigned int*) malloc(arr_size * sizeof(unsigned int));
    size_t i, k;
    for(i = 0; i < arr_size; ++i)
        arr[i] = 10;

    unsigned int result = 0;
    clock_t start, end;
    const int c_iterations = 10;    // iterations of experiment
    float faster_avg = 0;
    // -----------------------------------------------------------------


    for(k = 0; k < c_iterations; ++k) {
        result = 0; 

        // Sequential
        start = clock();

        for(i = 0; i < arr_size; i += 8) {
            result += arr[i+0] + arr[i+1] + 
                    arr[i+2] + arr[i+3] + 
                    arr[i+4] + arr[i+5] +
                    arr[i+6] + arr[i+7];
        }

        end = clock();
        const float c_time_seq = (float)(end - start)/CLOCKS_PER_SEC;   
        printf("seq: %f sec, res: %u, ", c_time_seq, result);
        // -----------------------------------------------------------------

        result = 0;

        // IPL-optimization
        start = clock();

        register unsigned int v0, v1, v2, v3;
        v0 = v1 = v2 = v3 = 0;

        for(i = 0; i < arr_size; i += 8) {

            v0 += arr[i+0] + arr[i+1];
            v1 += arr[i+2] + arr[i+3];
            v2 += arr[i+4] + arr[i+5];
            v3 += arr[i+6] + arr[i+7];


        }
        result = v0+v1+v2+v3;


        end = clock();
        const float c_time_ipl = (float)(end - start)/CLOCKS_PER_SEC;
        const float c_faster = c_time_seq/c_time_ipl;

        printf("ipl: %f sec, faster %f X, res: %u \n", c_time_ipl, c_faster, result);           
        faster_avg += c_faster;
    }

    faster_avg = faster_avg/c_iterations;
    printf("faster AVG: %f \n", faster_avg);

    return 0;
}

更新

  • 顺序(反汇编程序MS Visual Studio 2013)
    for (i = 0; i < arr_size; i += 8) {
        result += arr[i + 0] + arr[i + 1] +
            arr[i + 2] + arr[i + 3] +
            arr[i + 4] + arr[i + 5] +
            arr[i + 6] + arr[i + 7];
    }

000000013F131080  mov         ecx,dword ptr [rdx-18h]  
000000013F131083  lea         rdx,[rdx+20h]  
000000013F131087  add         ecx,dword ptr [rdx-34h]  
000000013F13108A  add         ecx,dword ptr [rdx-30h]  
000000013F13108D  add         ecx,dword ptr [rdx-2Ch]  
000000013F131090  add         ecx,dword ptr [rdx-28h]  
000000013F131093  add         ecx,dword ptr [rdx-24h]  
000000013F131096  add         ecx,dword ptr [rdx-1Ch]  
000000013F131099  add         ecx,dword ptr [rdx-20h]  
000000013F13109C  add         edi,ecx  
000000013F13109E  dec         r8  
000000013F1310A1  jne         main+80h (013F131080h)  
  • ILP(反汇编程序MS Visual Studio 2013)
    for (i = 0; i < arr_size; i += 8) {
        v0 += arr[i + 0] + arr[i + 1];
000000013F1310F0  mov         ecx,dword ptr [rdx-0Ch]  
        v1 += arr[i + 2] + arr[i + 3];
        v2 += arr[i + 4] + arr[i + 5];
000000013F1310F3  mov         eax,dword ptr [rdx+8]  
000000013F1310F6  lea         rdx,[rdx+20h]  
000000013F1310FA  add         ecx,dword ptr [rdx-28h]  
000000013F1310FD  add         eax,dword ptr [rdx-1Ch]  
000000013F131100  add         ebp,ecx  
000000013F131102  mov         ecx,dword ptr [rdx-24h]  
000000013F131105  add         ebx,eax  
000000013F131107  add         ecx,dword ptr [rdx-20h]  
        v3 += arr[i + 6] + arr[i + 7];
000000013F13110A  mov         eax,dword ptr [rdx-10h]  
        v3 += arr[i + 6] + arr[i + 7];
000000013F13110D  add         eax,dword ptr [rdx-14h]  
000000013F131110  add         esi,ecx  
000000013F131112  add         edi,eax  
000000013F131114  dec         r8  
000000013F131117  jne         main+0F0h (013F1310F0h) 
    }
    result = v0 + v1 + v2 + v3;

编译器命令行:

/GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /Ob2 /sdl /Fd"x64\Release\vc120.pdb" /fp:precise /D "_MBCS" /errorReport:prompt /WX- /Zc:forScope /Gd /Oi /MT /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" /Ot /Fp"x64\Release\IPL_reduce_test.pch" 

答案的补充说明:

这个简单的例子展示了Unroll-loop和Unroll-loop + ILP之间ILP对50000000双元素数组的好处:http://ideone.com/LgTP6b

  

更快的AVG:1.152778

  • False-Sequential 可以通过CPU管道进行优化(反汇编程序MS Visual Studio 2013) - 在每次迭代中添加8个元素使用临时寄存器xmm0然后添加到结果中xmm6,即可以使用Register renaming
result += arr[i + 0] + arr[i + 1] + arr[i + 2] + arr[i + 3] +
    arr[i + 4] + arr[i + 5] + arr[i + 6] + arr[i + 7];
000000013FBA1090  movsd       xmm0,mmword ptr [rcx-10h]  
000000013FBA1095  add         rcx,40h  
000000013FBA1099  addsd       xmm0,mmword ptr [rcx-48h]  
000000013FBA109E  addsd       xmm0,mmword ptr [rcx-40h]  
000000013FBA10A3  addsd       xmm0,mmword ptr [rcx-38h]  
000000013FBA10A8  addsd       xmm0,mmword ptr [rcx-30h]  
000000013FBA10AD  addsd       xmm0,mmword ptr [rcx-28h]  
000000013FBA10B2  addsd       xmm0,mmword ptr [rcx-20h]  
000000013FBA10B7  addsd       xmm0,mmword ptr [rcx-18h]  
000000013FBA10BC  addsd       xmm6,xmm0  
000000013FBA10C0  dec         rdx  
000000013FBA10C3  jne         main+90h (013FBA1090h) 
  • True-Sequential 无法通过CPU管道优化(反汇编程序MS Visual Studio 2013) - 在每次迭代中添加8个元素使用结果寄存器xmm6,即不能使用Register renaming
            result += arr[i + 0];
000000013FFC1090  addsd       xmm6,mmword ptr [rcx-10h]  
000000013FFC1095  add         rcx,40h  
            result += arr[i + 1];
000000013FFC1099  addsd       xmm6,mmword ptr [rcx-48h]  
            result += arr[i + 2];
000000013FFC109E  addsd       xmm6,mmword ptr [rcx-40h]  
            result += arr[i + 3];
000000013FFC10A3  addsd       xmm6,mmword ptr [rcx-38h]  
            result += arr[i + 4];
000000013FFC10A8  addsd       xmm6,mmword ptr [rcx-30h]  
            result += arr[i + 5];
000000013FFC10AD  addsd       xmm6,mmword ptr [rcx-28h]  
            result += arr[i + 6];
000000013FFC10B2  addsd       xmm6,mmword ptr [rcx-20h]  
            result += arr[i + 7];
000000013FFC10B7  addsd       xmm6,mmword ptr [rcx-18h]  
000000013FFC10BC  dec         rdx  
000000013FFC10BF  jne         main+90h (013FFC1090h) 

2 个答案:

答案 0 :(得分:15)

在大多数英特尔处理器上,进行浮点数添加需要3个周期。但如果它们是独立的,它可以维持1 /周期。

我们可以通过在关键路径上添加浮点数来轻松演示ILP。


<强>环境:

  • GCC 4.8.2:-O2
  • Sandy Bridge Xeon

确保编译器不进行不安全的浮点优化。

#include <iostream>
using namespace std;

#include <time.h>

const int iterations = 1000000000;

double sequential(){
    double a = 2.3;
    double result = 0;

    for (int c = 0; c < iterations; c += 4){
        //  Every add depends on the previous add. No ILP is possible.
        result += a;
        result += a;
        result += a;
        result += a;
    }

    return result;
}
double optimized(){
    double a = 2.3;
    double result0 = 0;
    double result1 = 0;
    double result2 = 0;
    double result3 = 0;

    for (int c = 0; c < iterations; c += 4){
        //  4 independent adds. Up to 4 adds can be run in parallel.
        result0 += a;
        result1 += a;
        result2 += a;
        result3 += a;
    }

    return result0 + result1 + result2 + result3;
}

int main(){

    clock_t start0 = clock();
    double sum0 = sequential();
    clock_t end0 = clock();
    cout << "sum = " << sum0 << endl;
    cout << "sequential time: " << (double)(end0 - start0) / CLOCKS_PER_SEC << endl;

    clock_t start1 = clock();
    double sum1 = optimized();
    clock_t end1 = clock();
    cout << "sum = " << sum1 << endl;
    cout << "optimized time:  " << (double)(end1 - start1) / CLOCKS_PER_SEC << endl;

}

<强>输出:

sum = 2.3e+09
sequential time: 0.948138
sum = 2.3e+09
optimized time:  0.317293

注意差异几乎是3倍。这是因为浮点数的3周期延迟和1周期吞吐量。

顺序版本的ILP非常少,因为所有浮点数都在关键路径上。 (每个add都需要等到上一次添加完成)展开的版本有4个独立的依赖链,最多有4个独立的添加 - 所有这些都可以并行运行。只需要3个就可以使处理器内核饱和。

答案 1 :(得分:4)

对于整数代码,也可以看到差异,例如

global cmp1
proc_frame cmp1
[endprolog]
    mov ecx, -10000000
    mov r8d, 1
    xor eax, eax
_cmp1_loop:
    add eax, r8d
    add eax, r8d
    add eax, r8d
    add eax, r8d
    add ecx, 1
    jnz _cmp1_loop
    ret
endproc_frame

global cmp2
proc_frame cmp2
[endprolog]
    mov ecx, -10000000
    mov r8d, 1
    xor eax, eax
    xor edx, edx
    xor r9d, r9d
    xor r10d, r10d
_cmp2_loop:
    add eax, r8d
    add edx, r8d
    add r9d, r8d
    add r10d, r8d
    add ecx, 1
    jnz _cmp2_loop
    add r9d, r10d
    add eax, edx
    add eax, r9d
    ret
endproc_frame

我的4770K的结果是第一个的TSC蜱约为3590万个,而第二个的豁免为1190万个(最短时间超过1k)。

在第一个中,eax上的依赖链是每次迭代4个周期中最慢的。没有其他问题,ecx上的依赖链更快,并且有足够的吞吐量来隐藏它和控制流。顺便提一下,35.9万个TSC滴答工作达到4000万个周期,因为TSC的基本时钟频率为3.5GHz,但最大涡轮增压为3.9GHz,3.9 / 3.5 * 35.9约为40个。

我在评论中提到的第二个版本(4个累加器,但使用[rsp]来存储常量1)需要17.9,这使得每次迭代2个周期。这与内存负载的吞吐量相匹配,在Haswell上是2 /周期。 4个负载,所以2个周期。循环开销仍然可以隐藏。

上面发布的第二个每次迭代需要1.3333个周期。前四个添加可以转到端口0,1,5和6,add/jnz融合对只能到端口6。将融合对放入p6离开3个端口4μs,因此1.3333个循环。