在C ++中通过引用传递char的效率

时间:2014-04-09 06:51:24

标签: c++ parameter-passing pass-by-reference pass-by-value

我正和朋友讨论以下功能原型:

void str_buf_append(const char&);

目的只是将字符添加到字符串缓冲区以及与当前问题无关的其他一些任务,即:假设我们不修改输入字符,最好通过引用传递或通过引用值?

我朋友的论点是,如果你通过引用传递一个字符,你就会把一个大小为int的东西放到堆栈上,而如果你按值传递,你只需要在那里放一些大小只有一个字节的东西

在我看来,这不是全局:当你通过价值时,我认为你实际上是在做以下事情:

  1. 在内存中某个位置创建一个与原始角色不同的角色副本。
  2. 当调用该函数时,将对复制字符的引用推入堆栈,因此不会保存任何内容,因为在引擎盖下,您仍然使用指针 - 只是指向不同内存位置的指针。
  3. 所以我的结论/意见是,在这种情况下,通过引用而不是通过值传递确实更有效。

    谁是对的?

5 个答案:

答案 0 :(得分:3)

如果函数是在同一个翻译单元中定义的(并且原型只是一个前向声明)那么它并不重要,编译器很可能会内联函数而你不能够说出不同之处。

如果函数在另一个转换单元(外部链接)中定义,则编译器会生成函数调用。大多数调用约定都会传递寄存器中的前几个参数,这肯定是字符或字符引用的情况。如果你传递值,编译器会将字符加载到第一个参数的寄存器中,如果你通过引用传递,编译器会将字符的地址放在寄存器中作为第一个参数,被调用的函数将从中加载字符那个地址。哪个更有效率?可能通过价值传递,但在今天的CPU中,每个周期都会发送无序执行和多个指令,现实情况是你可能无法区分它们。

这是一个简单的c ++程序,可以看到gcc在Linux上生成的内容:

extern char byvalue( char );
extern char byref( const char & );
int main( int argc, char * argv[] )
{
    char c = byvalue( argv[0][0] ) + byref( argv[0][1] );
    return c;
}

我编译并查看了生成的代码:

$ g++ -O3 param.cpp -c -o param.o
$ objdump -D param.o|less

这里有两个调用生成的代码在函数main中的样子 - %rdi /%edi是第一个(在这种情况下只有)参数的寄存器:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   53                      push   %rbx
   2:   48 89 f3                mov    %rsi,%rbx
   5:   48 83 ec 08             sub    $0x8,%rsp
   9:   48 8b 06                mov    (%rsi),%rax
   c:   0f be 38                movsbl (%rax),%edi     ; %edi is character
   f:   e8 00 00 00 00          callq  14 <main+0x14>  ; byvalue
  14:   48 8b 3b                mov    (%rbx),%rdi
  17:   89 c5                   mov    %eax,%ebp
  19:   48 83 c7 01             add    $0x1,%rdi       ; %rdi is address of character
  1d:   e8 00 00 00 00          callq  22 <main+0x22>  ; byref
  22:   48 83 c4 08             add    $0x8,%rsp
  26:   01 e8                   add    %ebp,%eax
  28:   5b                      pop    %rbx
  29:   0f be c0                movsbl %al,%eax
  2c:   5d                      pop    %rbp
  2d:   c3                      retq   

正如您所见,编译器生成的代码可以加载字符

   c:   0f be 38                movsbl (%rax),%edi     ; %edi is character
   f:   e8 00 00 00 00          callq  14 <main+0x14>  ; byvalue

或者加载角色的地址

  19:   48 83 c7 01             add    $0x1,%rdi       ; %rdi is address of character
  1d:   e8 00 00 00 00          callq  22 <main+0x22>  ; byref

答案 1 :(得分:1)

事实是,您无法预测优化发生后的情况;唯一能保持&#34;固定&#34;是代码的语义,而不是它实际执行的方式。

答案 2 :(得分:0)

你们两个都错了。

引用需要指向原始对象的指针。不是int。可能是64位。

一个char被压入堆栈,而不是复制到内存中的其他位置,并且使用标准打包这可能就像一个int也是64位。

有问题的指针必须在以后得到参考值,以便在大多数硬件上获取64字节的整个高速缓存行,如果它尚未在调用的高速缓存中。您需要拉入相同的缓存行以将其推入堆栈,因此差别很小。但是如果char存储在寄存器中,则可能已经将其推入堆栈而没有读入缓存行。

如果你的速度优化,如果它不是参考,它可能会保留在同一个寄存器中。智能编译器的人可能会发现你做了一些愚蠢的事情,比如通过const引用传递pod类型并将其保存在寄存器中以使你看起来很好,但是你不应该总是依赖编译器来让你看起来很好。

除非您担心有人可能会意外更改此函数中char的值,否则为什么要将它作为const ref传递?

每个编译器/平台都是不同的,有时参考可能会花费更多,但对于pod类型,传递我的价值将永远不会超过参考。

所以是的,你们两个都错了。

答案 3 :(得分:0)

这是通过引用传递时fun (x);行发生的情况的草图:

void fun (char const & c) {use (c);}
...
fun (x);
[next line]
  1. 将回复指针[next line]放到堆栈上,让我们说内存地址A。它是4或8个字节。
  2. p指向x的指针放在堆栈上,让我们说内存地址为B。它是4或8个字节。
  3. 使用c时,其内存位置为[dereference [dereference B]]
  4. 返回[dereference A]
  5. 以下是传递值时在fun (x);行发生的事情:

    void fun (char const c) {use (c);}
    ...
    fun (x);
    [next line]
    
    1. 将回复指针[next line]放到堆栈上,让我们说内存地址A。它是4或8个字节。
    2. c的{​​{1}}副本放入堆栈,然后说内存地址x。这是1个字节。
    3. 使用B时,其内存位置为c
    4. 返回[dereference B]
    5. 地址[dereference A]A(相对于堆栈顶部)被硬编码到编译器生成的二进制可执行文件中。 第2步和第3步的差异是大小和单引号或双引号,都有利于传递值。

      也就是说,启用优化的现代编译器可能会通过内联函数来优化上述两个程序 - 如果它足够简单,可以生成以下内容:

      1. ---
      2. ---
      3. 使用B作为c的参数时,会使用fun
      4. ---

答案 4 :(得分:0)

你错了。没有1字节的堆栈槽。但是当您通过引用传递char时:

  1. 您必须计算其地址。如果它是静态的,那就是常数。如果它在对象中,则必须向对象的地址添加偏移量。如果它位于堆栈上,方法本地,则必须将其堆栈帧偏移量添加到当前堆栈帧指针。在x86上,所有这些都是用LEA指令完成的,但你是在英特尔硬件上吗?
  2. 然后你必须推送地址。
  3. 然后,每次在目标方法中使用它时,都必须取消引用它。
  4. 所有这些都是更多的内存引用,而不仅仅是将值压入堆栈。

    在一个非平凡的方法中是否真的重要是另一个问题。当然,在某些情况下,编译器可以将其编译为pass-by-reference。