这种内联结果是否常见?

时间:2017-12-10 11:49:48

标签: c optimization inline inlining

由于大学工作,我必须调查一个简单的优化,内联。

以下是基本代码:

#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计算整个时间,而不仅仅是程序本身使用的时间,但我需要专门使用该函数。

提前致谢!

1 个答案:

答案 0 :(得分:3)

让我们分析两个版本代码的GCC 7.2( @Override public boolean onOptionsItemSelected(MenuItem item) { View view = new View(this); if (item.getItemId() == R.id.action_save){ insertRecord(view); } return super.onOptionsItemSelected(item); } )生成的汇编输出。

没有内联

首先,让我们检查计算机必须完成多少工作才能通过单独的功能完成任务:

O0

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

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

代码的处理部分大约需要32条指令(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 L4之间的指令以及L3函数的指令)。

大多数指令用于进行函数调用。

理解函数调用如何工作的简单方法是:

  1. 参数被推送到调用堆栈
  2. 返回地址被推送到调用堆栈
  3. 该函数被称为
  4. 制作帧指针的副本
  5. 为当地人腾出空间
  6. 执行实际功能代码
  7. 恢复函数调用之前的状态
  8. 返回来电者
  9. 上述步骤(第6步除外)需要额外的说明来进行所需的处理。这称为函数调用开销。

    使用内联

    现在让我们检查计算机内联函数需要做多少工作。

    add

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

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

    处理代码(标签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之间的说明)有大约14条指令。在这个汇编输出中,所有负责进行函数调用的指令都不存在,这样可以节省大量的CPU周期。

    通常,当函数的运行时间超过函数调用开销的几倍时,函数调用的开销不相关。在您的代码中,函数的运行时间非常短,因此函数调用开销具有重要意义。

    如果使用L2标志,编译器确实会为您进行内联。您可以通过查看使用O1生成的汇编来查找,也可以直接查看使用O1尝试的list of optimizations的GCC手册。

    您可以使用O1标志生成汇编输出,或者您可以使用GodBolt在线进行汇编输出(此汇总输出来自此处)。