只是将循环代码移到函数中,代码运行速度变慢了十倍

5
我正在运行下面的代码,它实际上没有做什么。它只是将2和4相加100万次,并输出运行时间。
#include "time.h"
#include <iostream>
using namespace std;

void add (){
int tot = 2+4;
}

void main(){
int t = clock();
int count = 0;
while(count<100000000){
    int tot = 2+4;
    count++;
}
cout <<endl<< "runtime = " << fixed <<(double) (clock() - t) / CLOCKS_PER_SEC <<"s" << endl;

但我很想看到在执行相同操作时调用函数所需的时间差异。因此,我将“int tot = 2+4”的行替换为“add()”。

我原本期望第二个运行时间会稍微长一点,但实际上比较长。第一个实现 = .3s,第二个实现 = 3s

我知道调用函数需要使用堆栈来存储返回地址和存储本地数据。但是它一定做了很多更多的事情吗?

如果有人能够解释一下究竟是什么导致了运行时的巨大差异,或者我可能做了一些傻事,那就太好了。


5
你为什么不自己比较编译的汇编代码输出呢?并且,尝试打开优化后再试一次! - Kerrek SB
5
你应该意识到编译器几乎肯定会在编译时优化掉对 6 的加法操作。 - Cameron
4
你的测量结果可能没有意义。编译器可以根据“as-if”规则丢弃所有没有副作用的未使用代码,内联函数调用,展开循环等,这完全取决于你的编译器在优化方面的能力有多强。 - Gene Bushuyev
1
打开优化选项(在GCC中为-O3)。如果没有打开优化选项,编译器甚至不会尝试生成快速代码 - 相反,它会添加一堆额外的指令,使调试更容易,但会减慢许多常见操作。特别是函数调用在-O0下比-O3慢得多。 - Crashworks
什么编译器和什么构建设置?函数调用的成本高度依赖于两者。很明显你没有使用任何优化,但知道使用什么编译器(和CPU)仍然是有趣的。 - Bruce Dawson
显示剩余3条评论
3个回答

5

正如Seth所提到的,内联函数很可能会被优化。

在第一种情况下(启用基本优化),很可能不会不断添加这两个数字,而是将2 + 4解析为6,并仅执行一个简单的操作。

mov eax, 6 ;eax is just an example here or
mov tot_addr, 6 ; mem->mem mov

在第二种情况下,由于是函数调用,系统必须进行以下操作:
push 4 ;assuming 4 byte ints
push 4 ;wrote 2 to be clear that you have to push both variables
call add

或者类似这样。由于需要为该函数创建调用堆栈(为简单起见,省略返回值推送等),因此成本更高。一旦此函数返回,堆栈指针需要移回第一个推送之前,然后RET将设置指令指针。正如您所看到的,这比执行简单操作更加昂贵。
mov eax, 4
add eax, 2

如果你只是进行简单的(未经优化的加法)操作,很可能会出现这种情况。

编辑:以下是有关内联函数的更多信息。当您内联函数时,它只是将函数本身执行的任何功能并将指令直接放置在引用它的位置,而不是进行调用指令和直接设置函数调用。例如,不是

mov eax, 4
mov ecx, 2
push 4 ; size for return value
push eax
push ecx
call add

你最终将得到
mov eax, 4
mov exc, 2
add eax, ecx

在代码术语中:
int a = 4;
int b = 2;
int res = add(a, b);

将会变成

int a = 4;
int b = 2;
int res = a + b;

假设您将add函数内联。

1
并不是真的,编译器不需要做所有这些微观优化,它只是丢弃所有未使用的代码,为整个程序生成一个单独的行:return 0; - Gene Bushuyev
@Gene:但是OP在谈论函数性能和使用函数与内联功能。我知道编译器不关心未使用的代码,只是将其丢弃,但为什么整个程序都变成了return 0;? - Jesus Ramos
@Gene:没事了,我仔细看了一下代码,发现由于循环什么也不做,它基本上就消失了。 - Jesus Ramos

2

相对于其他操作,函数调用是非常昂贵的(比你想象的还要昂贵),正如你所提到的原因。尝试将函数声明为inline并/或启用优化以恢复性能。


0

函数调用确实会给CPU带来比仅推送返回地址和参数更多的工作。

在某些CPU上,您可能会遇到管道停顿。甚至在读取下一条指令时发生i-cache缺失。这些可能会导致比您注意到的10倍以上的减速。

这就是为什么编译器会尽最大努力优化函数调用的原因。


您更有可能因为函数调用而产生页面错误,而不是内联功能。缓存未命中也可能发生(下一条指令不一定具有良好的局部性,这也解释了错误)。我没有详细讨论这个问题,因为汇编语言可能已经超出了原帖作者的预期。 - Jesus Ramos
@Jesus Ramos:我提到了缓存未命中(页面错误是缓存未命中的一种形式),是的,缺乏局部性是未命中的原因。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接