2

由于大学工作,我不得不研究一个简单的优化,即内联。

这是基本代码:

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

#define ITER 1000
#define N 3000000

int i, j;
float x[N], y[N], z[N];

void add(float x, float y, float *z){
    *z = x + y;
}

void initialVersion(){
    struct timeval inicio, final;
    double time;

    gettimeofday(&inicio, 0);
    for(j = 0; j < ITER; j++){
        for(i = 0; i < N; i++){
            add(x[i], y[i], &z[i]);
        }
    }

    gettimeofday(&final, 0);

    time = (final.tv_sec - inicio.tv_sec + (final.tv_usec - inicio.tv_usec)/1.e6);

    printf("Time: %f\n", time);

}

这是带有内联的代码:

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

#define ITER 1000
#define N 3000000

int i, j;
float x[N], y[N], z[N];

void inliningVersion(){
    struct timeval inicio, final;
    double time;

    gettimeofday(&inicio, 0);
    for(j = 0; j < ITER; j++){
        for(i = 0; i < N; i++){
            z[i] = x[i] + y[i];
        }
    }

    gettimeofday(&final, 0);

    time = (final.tv_sec - inicio.tv_sec + (final.tv_usec - inicio.tv_usec)/1.e6);

    printf("Time: %f\n", time);

}

使用选项 -O0 和 gcc 进行编译,基本版本的结果是 14.27 秒,内联版本的结果是 4.45 秒。这很常见吗?我执行了该程序 10 次,结果总是相似的。你怎么看?

然后,使用选项 -O1 编译两个版本的结果相似,大约为 1.5 秒,所以我gcc 为我使用 O1 进行内联。

顺便说一句,我知道 gettimeofday 计算总时间,而不仅仅是程序本身使用的时间,而且我需要专门使用该功能。

提前致谢!

4

1 回答 1

3

让我们分析一下 GCC 7.2(带有O0)为两个版本的代码生成的汇编输出。


没有内联

首先,让我们检查一下计算机需要完成多少工作才能使用单独的函数完成任务:

void add(float x, float y, float *z){
    *z = x + y;
}

int main ()
{
    float x[100], y[100], z[100];
    for(int i = 0; i < 100; i++){
             add(x[i], y[i], &z[i]);
        }
}

对于上述代码,GCC 会生成如下所示的程序集:

add(float, float, float*):
        pushq   %rbp
        movq    %rsp, %rbp
        movss   %xmm0, -4(%rbp)
        movss   %xmm1, -8(%rbp)
        movq    %rdi, -16(%rbp)
        movss   -4(%rbp), %xmm0
        addss   -8(%rbp), %xmm0
        movq    -16(%rbp), %rax
        movss   %xmm0, (%rax)
        nop
        popq    %rbp
        ret
main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $1224, %rsp
        movl    $0, -4(%rbp)
.L4:
        cmpl    $99, -4(%rbp)
        jg      .L3
        leaq    -1216(%rbp), %rax
        movl    -4(%rbp), %edx
        movslq  %edx, %rdx
        salq    $2, %rdx
        addq    %rax, %rdx
        movl    -4(%rbp), %eax
        cltq
        movss   -816(%rbp,%rax,4), %xmm0
        movl    -4(%rbp), %eax
        cltq
        movl    -416(%rbp,%rax,4), %eax
        movq    %rdx, %rdi
        movaps  %xmm0, %xmm1
        movl    %eax, -1220(%rbp)
        movss   -1220(%rbp), %xmm0
        call    add(float, float, float*)
        addl    $1, -4(%rbp)
        jmp     .L4
.L3:
        movl    $0, %eax
        leave
        ret

代码的处理部分大约需要 32 条指令(函数之间L4L3指令add)。

大部分指令用于进行函数调用。

了解函数调用如何工作的简化方法是:

  1. 参数被压入调用堆栈
  2. 返回地址被压入调用栈
  3. 该函数被调用
  4. 复制帧指针
  5. 为堆栈上的本地人腾出空间
  6. 实际功能代码被执行
  7. 恢复函数调用之前的状态
  8. 返回给调用者

上述步骤(第 6 步除外)需要额外的指令来进行所需的处理。这称为函数调用开销。


带内联

现在让我们检查一下如果函数是内联的,计算机需要做多少工作。

int main ()
{
    float x[100], y[100], z[100];
    for(int i = 0; i < 100; i++){
            z[i] = x[i] + y[i];
        }
}

对于上面的代码,GCC 会产生一个汇编输出,如下所示:

main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $1096, %rsp
        movl    $0, -4(%rbp)
.L3:
        cmpl    $99, -4(%rbp)
        jg      .L2
        movl    -4(%rbp), %eax
        cltq
        movss   -416(%rbp,%rax,4), %xmm1
        movl    -4(%rbp), %eax
        cltq
        movss   -816(%rbp,%rax,4), %xmm0
        addss   %xmm1, %xmm0
        movl    -4(%rbp), %eax
        cltq
        movss   %xmm0, -1216(%rbp,%rax,4)
        addl    $1, -4(%rbp)
        jmp     .L3
.L2:
        movl    $0, %eax
        leave
        ret

处理代码(标签L3和之间的指令L2)有大约 14 条指令。在这个汇编输出中,负责进行函数调用的所有指令都不存在,这节省了大量的 CPU 周期。

通常,当您的函数的运行时间是函数调用开销的数倍以上时,函数调用的开销无关紧要。在您的代码中,函数的运行时间非常短,因此函数调用开销变得非常重要。

如果您使用该O1标志,编译器确实会为您进行内联。您可以通过检查使用 生成的程序集来查找,O1或者您可以直接查看 GCC 手册以获取使用 .尝试的优化列表O1

-S您可以使用标志生成程序集输出,也可以使用GodBolt在线生成(程序集输出取自此处用于本文)。

于 2017-12-10T13:32:16.723 回答