按值传递比按引用传递更快

Pass by value faster than pass by reference

本文关键字:按引用传递 按值传递      更新时间:2023-10-16

我用 c++ 做了一个简单的程序来比较两种方法之间的性能 - 按值传递和按引用传递。实际上,按值传递比按引用传递执行得更好。

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

如果有人能详细解释为什么按值传递需要更少的时钟周期,我将非常高兴。

#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 中。最后,我们将值乘以 5。

让我们看一下第二个额外的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操作。

通过引用传递的开销:

  • 每次访问都需要取消引用,即多读取一次内存

按值传递的开销:

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

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

想象一下,你走进一个函数,你应该带着一个 int 值进来。函数中的代码想要使用该 int 值执行操作。

按值传递就像走进函数,当有人要求 int foo 值时,您只需将其提供给他们即可。

通过引用传递是使用int foo值的地址进入函数。现在,每当有人需要foo的价值时,他们都必须去查找它。每个人都会抱怨不得不一直取消引用foo。我已经在这个函数中使用了 2 毫秒,我一定已经查找了一千次 foo!你当初为什么不给我价值?你为什么不按值传递?

这个类比帮助我明白了为什么按价值传递通常是最快的选择。

对于一些推理:在大多数流行的机器中,整数是 32 位,指针是 32 或 64 位

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

要乘以整数,您必须:

乘以它。

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

尊重指针。乘以它。

希望它足够清楚:)


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

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

很难说你的基准测试有多有效,因为编译器包含了各种优化。(当然你可以控制编译器的自由,但你没有提供这方面的信息)

最后(可能是最重要的),指针、值或引用没有与之相关的速度。谁知道呢,您可能会发现一台使用指针速度更快的机器,并且在值方面花费了很长时间,或者相反。好的,好的,硬件中有一些模式,我们做出所有这些假设,最广泛接受的似乎是:

按值传递简单对象,按引用(或指针)传递更复杂的对象(但话又说回来,什么是复杂的?什么简单?它随时间变化,硬件如下)

所以最近我感觉到标准意见正在变成:按值传递并信任编译器。这很酷。编译器以多年的专业知识开发和愤怒的用户为后盾,要求它总是更好。

当您按值传递时,您告诉编译器创建您按值传递的实体的副本。

通过引用传递时,您告诉编译器它必须使用引用指向的实际内存。 编译器不知道您这样做是为了进行优化,还是因为引用的值可能在某个其他线程中更改(例如)。 它必须使用该内存区域。

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

最后,根据计算机的体系结构和传递的类型,引用实际上可能大于要复制的值。 复制 32 位整数涉及的复制少于在 64 位计算机上传递引用。

因此,仅当您需要引用(以更改值,

或者因为该值可能在其他地方更改)时,或者当复制引用的对象比取消引用必要的内存更昂贵时,才应通过引用进行传递。

虽然最后一点不是平凡的,但一个好的经验法则是做Java所做的事情:按值传递基本类型,通过(const)引用传递复杂类型。

对于小型类型,按值传递通常非常快,因为它们中的大多数都比现代系统上的指针小(64 位)。按值传递时,也可能进行某些优化。

作为一般规则,按值传递内置类型。

在这种情况下,编译器可能意识到乘法的结果没有在按值传递的情况下使用,并完全优化了它。如果不看到反汇编的代码,就不可能确定。

在本机 64 位平台上,执行 32 位内存操作指令通常较慢,因为处理器无论如何都必须运行 64 位指令。如果编译器正确完成,则 32 位指令在指令缓存处"配对",但如果使用 64 位指令执行 32 位读取,则会复制 4 个额外的字节作为填充,然后丢弃。简而言之,小于指针大小的值并不一定意味着它更快。这取决于情况和编译器,绝对不应考虑性能,除非复合类型的值肯定比指针大 1 个量级,或者您需要为一个特定平台提供绝对最佳性能而不考虑可移植性的情况。在按引用传递还是按值传递之间的选择应仅取决于您是否希望被调用的过程能够修改传递的对象。如果它只是小于 128 位的类型(按值传递)的读取,则更安全。