由于大学工作的原因,我必须研究一个简单的优化,即内联。
这是基本代码:
#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);
}
使用 gcc 选项 -O0 进行编译,基本版本的结果为 14.27 秒,内联版本的结果为 4.45 秒。这很常见吗?我执行了该程序 10 次,结果总是相似的。你觉得怎么样?
然后,使用选项 -O1 进行编译,两个版本的结果相似,大约 1.5 秒,因此我假设 gcc 使用 O1 为我进行内联。
顺便说一句,我知道 gettimeofday 计算的是总体时间,而不仅仅是程序本身使用的时间,但我需要专门使用该函数。
提前致谢!
最佳答案
让我们分析一下 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条指令(L4
和L3
之间的指令以及add
函数的指令)。
大部分指令用于进行函数调用。
了解函数调用如何工作的简化方法是:
- 参数被压入调用堆栈
- 返回地址被推送到调用堆栈
- 调用该函数
- 复制帧指针
- 为堆栈中的本地元素腾出空间
- 执行实际函数代码
- 恢复函数调用前的状态
- 返回给调用者
上述步骤(第 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手册list of optimizations已使用 O1
进行了尝试。
您可以使用 -S
标志生成汇编输出,也可以使用 GodBolt 在线执行此操作。 (本文的汇编输出取自此处)。
关于c - 这种内联结果常见吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47738534/