我正和朋友讨论以下功能原型:
void str_buf_append(const char&);
目的只是将字符添加到字符串缓冲区以及与当前问题无关的其他一些任务,即:假设我们不修改输入字符,最好通过引用传递或通过引用值?
我朋友的论点是,如果你通过引用传递一个字符,你就会把一个大小为int的东西放到堆栈上,而如果你按值传递,你只需要在那里放一些大小只有一个字节的东西
在我看来,这不是全局:当你通过价值时,我认为你实际上是在做以下事情:
所以我的结论/意见是,在这种情况下,通过引用而不是通过值传递确实更有效。
谁是对的?
答案 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]
[next line]
放到堆栈上,让我们说内存地址A
。它是4或8个字节。p
指向x
的指针放在堆栈上,让我们说内存地址为B
。它是4或8个字节。c
时,其内存位置为[dereference [dereference B]]
。[dereference A]
。以下是传递值时在fun (x);
行发生的事情:
void fun (char const c) {use (c);}
...
fun (x);
[next line]
[next line]
放到堆栈上,让我们说内存地址A
。它是4或8个字节。c
的{{1}}副本放入堆栈,然后说内存地址x
。这是1个字节。B
时,其内存位置为c
。[dereference B]
。地址[dereference A]
和A
(相对于堆栈顶部)被硬编码到编译器生成的二进制可执行文件中。
第2步和第3步的差异是大小和单引号或双引号,都有利于传递值。
也就是说,启用优化的现代编译器可能会通过内联函数来优化上述两个程序 - 如果它足够简单,可以生成以下内容:
B
作为c
的参数时,会使用fun
。答案 4 :(得分:0)
你错了。没有1字节的堆栈槽。但是当您通过引用传递char时:
所有这些都是更多的内存引用,而不仅仅是将值压入堆栈。
在一个非平凡的方法中是否真的重要是另一个问题。当然,在某些情况下,编译器可以将其编译为pass-by-reference。