通过值比通过引用更快

我在c ++中做了一个简单的程序来比较两种方法之间的性能 – 传递值和传递参考。 其实通过价值performance比通过参考更好。

结论应该是传递值需要更less的时钟周期(指令)

如果有人能够详细解释为什么按价值传递需要更less的时钟周期,我会很高兴。

#include <iostream> #include <stdlib.h> #include <time.h> using namespace std; void function(int *ptr); void function2(int val); int main() { int nmbr = 5; clock_t start, stop; start = clock(); for (long i = 0; i < 1000000000; i++) { function(&nmbr); //function2(nmbr); } stop = clock(); cout << "time: " << stop - start; return 0; } /** * pass by reference */ void function(int *ptr) { *ptr *= 5; } /** * pass by value */ void function2(int val) { val *= 5; } 

找出为什么有什么区别的好方法是检查反汇编。 以下是我在Visual Studio 2012的机器上得到的结果。

通过优化标志,这两个函数都会生成相同的代码:

 009D1270 57 push edi 009D1271 FF 15 D4 30 9D 00 call dword ptr ds:[9D30D4h] 009D1277 8B F8 mov edi,eax 009D1279 FF 15 D4 30 9D 00 call dword ptr ds:[9D30D4h] 009D127F 8B 0D 48 30 9D 00 mov ecx,dword ptr ds:[9D3048h] 009D1285 2B C7 sub eax,edi 009D1287 50 push eax 009D1288 E8 A3 04 00 00 call std::operator<<<std::char_traits<char> > (09D1730h) 009D128D 8B C8 mov ecx,eax 009D128F FF 15 2C 30 9D 00 call dword ptr ds:[9D302Ch] 009D1295 33 C0 xor eax,eax 009D1297 5F pop edi 009D1298 C3 ret 

这基本上相当于:

 int main () { clock_t start, stop ; start = clock () ; stop = clock () ; cout << "time: " << stop - start ; return 0 ; } 

没有优化标志,你可能会得到不同的结果。

函数(没有优化):

 00114890 55 push ebp 00114891 8B EC mov ebp,esp 00114893 81 EC C0 00 00 00 sub esp,0C0h 00114899 53 push ebx 0011489A 56 push esi 0011489B 57 push edi 0011489C 8D BD 40 FF FF FF lea edi,[ebp-0C0h] 001148A2 B9 30 00 00 00 mov ecx,30h 001148A7 B8 CC CC CC CC mov eax,0CCCCCCCCh 001148AC F3 AB rep stos dword ptr es:[edi] 001148AE 8B 45 08 mov eax,dword ptr [ptr] 001148B1 8B 08 mov ecx,dword ptr [eax] 001148B3 6B C9 05 imul ecx,ecx,5 001148B6 8B 55 08 mov edx,dword ptr [ptr] 001148B9 89 0A mov dword ptr [edx],ecx 001148BB 5F pop edi 001148BC 5E pop esi 001148BD 5B pop ebx 001148BE 8B E5 mov esp,ebp 001148C0 5D pop ebp 001148C1 C3 ret 

函数2(不优化)

 00FF4850 55 push ebp 00FF4851 8B EC mov ebp,esp 00FF4853 81 EC C0 00 00 00 sub esp,0C0h 00FF4859 53 push ebx 00FF485A 56 push esi 00FF485B 57 push edi 00FF485C 8D BD 40 FF FF FF lea edi,[ebp-0C0h] 00FF4862 B9 30 00 00 00 mov ecx,30h 00FF4867 B8 CC CC CC CC mov eax,0CCCCCCCCh 00FF486C F3 AB rep stos dword ptr es:[edi] 00FF486E 8B 45 08 mov eax,dword ptr [val] 00FF4871 6B C0 05 imul eax,eax,5 00FF4874 89 45 08 mov dword ptr [val],eax 00FF4877 5F pop edi 00FF4878 5E pop esi 00FF4879 5B pop ebx 00FF487A 8B E5 mov esp,ebp 00FF487C 5D pop ebp 00FF487D C3 ret 

为什么传递值更快(在没有优化的情况下)?

那么, function()有两个额外的mov操作。 我们来看看第一个额外的mov操作:

 001148AE 8B 45 08 mov eax,dword ptr [ptr] 001148B1 8B 08 mov ecx,dword ptr [eax] 001148B3 6B C9 05 imul ecx,ecx,5 

这里我们正在引用指针。 在function2 () ,我们已经有了这个值,所以我们避免了这一步。 我们首先将指针的地址移动到寄存器eax中。 然后我们将指针的值移到寄存器ecx中。 最后,我们乘以五。

我们来看看第二个额外的mov操作:

 001148B3 6B C9 05 imul ecx,ecx,5 001148B6 8B 55 08 mov edx,dword ptr [ptr] 001148B9 89 0A mov dword ptr [edx],ecx 

现在我们正在倒退。 我们刚刚完成乘以5,我们需要把值放回到内存地址。

由于function2 ()不必处理引用和取消引用指针,它会跳过这两个额外的mov操作。

通过引用传递开销:

  • 每个访问都需要一个解引用,也就是说,还有一个内存读取

开销与价值传递:

  • 该值需要被复制到堆栈或寄存器中

对于小对象,如整数,传递值将会更快。 对于更大的对象(例如大型结构),复制会产生太多的开销,所以通过引用传递会更快。

有一些推论:在大多数stream行的机器中,整数是32位,指针是32位或64位

所以你必须传递那么多的信息。

要乘以一个整数,你必须:

相乘。

要乘以指针指向的整数,您必须:

尊重指针。 相乘。

希望这是清楚的:)


现在来一些更具体的东西:

正如已经指出的那样,你的by-value函数对结果没有任何作用,但是by-pointer实际上将结果保存在内存中。 为什么你这么差的指针是不公平的? :( (开玩笑)

很难说你的基准是多么的有效,因为编译器会挤满所有的优化。 (当然你可以控制编译器的自由度,但是你没有提供这方面的信息)

最后(也许是最重要的),指针,值或引用没有相关的速度。 谁知道,你可能会发现一个机器,指针更快,花费很多时间与价值观,或相反。 好的,好吧,硬件上有一些模式,我们做出所有这些假设,最广泛接受的似乎是:

通过引用(或指针)传递简单对象的价值和更复杂的对象(但是,然后再一次,什么是复杂的?什么是简单的?随着硬件的变化随着时间的推移)

所以最近我感觉到标准意见正在成为:传递价值和信任编译器。 这很酷。 编译器经过多年的专业技术发展和愤怒的用户的支持,要求它始终更好。

当你通过价值传递时,你告诉编译器按照价值传递实体的副本。

当你通过引用传递时,你告诉编译器必须使用引用指向的实际内存。 编译器不知道你是在做这个尝试来优化,还是因为被引用的值可能在其他线程中发生了变化(例如)。 它必须使用该区域的内存。

按引用传递意味着处理器必须访问该特定的存储器块。 这可能是也可能不是最有效的过程,取决于寄存器的状态。 当通过引用传递时,可以使用堆栈上的内存,这就增加了访问caching(更快)内存的机会。

最后,根据机器的体系结构和传递的types,引用实际上可能大于要复制的值。 复制一个32位的整数涉及复制不到在64位机器上传递引用。

所以只有当你需要一个引用(要改变这个值,或者因为这个值可能在其他地方被改变了),或者当复制被引用的对象比引用必要的内存更昂贵的时候,引用的传递就应该完成。

尽pipe最后一点不是微不足道的,但一个好的经验法则是做Java的工作:通过值传递基本types,通过(const)引用传递复杂types。

在这种情况下,编译器可能已经意识到乘法的结果并没有被用在pass-by-value的情况下,而是完全地优化了它。 没有看到反汇编的代码是不可能的。

对于小型产品来说,价值传递通常是非常快的,因为它们大多数都比现代系统(64位)上的指针小。 按价值传递也可能会有某些优化。

作为一般规则,通过价值传递内buildtypes。

想象一下,你走进一个函数,你应该进来一个int值。 函数中的代码想要用那个int值做东西。

按值传递就像进入函数,当有人要求int foo的值时,就把它给它们。

通过引用传递到具有int foo值的地址的函数中。 现在,每当有人需要富有的价值,他们必须去查找它。 每个人都会抱怨不得不把所有的时间都浪费掉。 我已经在这个function2毫秒了,我一定已经看了上千次! 你为什么不把价值放在第一位呢? 你为什么不通过价值?

这个比喻帮助我明白为什么按价值传递往往是最快的select。

在本地64位平台上,经常执行32位内存操作指令的速度较慢,因为无论如何,处理器都必须运行64位指令。 如果编译器正确完成,则32位指令在指令高速caching中被“配对”,但是如果用64位指令执行32位读取,则4个附加字节被复制为填充,然后被丢弃。 总之,小于指针大小的值不一定意味着它更快。 这取决于情况和编译器,绝对不应该考虑到性能,除了复合types的值肯定大于指针大小1,或者在需要绝对最佳性能的情况下一个特定的平台,而不考虑可移植性。 通过引用或按值传递之间的select应该仅取决于是否希望被调用的过程能够修改传递的对象。 如果它只是一个小于128位的types的读取,按值传递,这是更安全的。