在 C++ 中通过引用传递 char 的效率
Posted
技术标签:
【中文标题】在 C++ 中通过引用传递 char 的效率【英文标题】:Efficiency of passing char by reference in C++ 【发布时间】:2014-05-22 04:54:45 【问题描述】:我正在和朋友讨论以下函数原型:
void str_buf_append(const char&);
目的只是将一个字符添加到字符串缓冲区以及与当前问题无关的一些其他任务,即:鉴于我们没有修改输入字符,是通过引用传递还是通过价值?
我的朋友的论点是,如果你通过引用传递一个 char,你把一个 int 大小的东西放到堆栈上,而如果你通过值传递,你只是把一个字节大小的东西放在那里.
不过,在我看来,这还不是全部:当您通过值传递时,我认为您实际上是在执行以下操作:
-
在内存中与原始角色不同的某个位置创建角色的副本。
调用函数时,将复制字符的引用压入堆栈,因此不会保存任何内容,因为在后台,您仍在使用指针——只是指向不同内存位置的指针。
所以我的结论/意见是,在这种情况下,通过引用传递确实比通过值传递更有效。
谁是对的?
【问题讨论】:
C++ 标准没有定义(取决于编译器/目标平台) 在这两种情况下,都可能使用寄存器。即使它们不是 ,您也可能会发现堆栈指针无论如何都会以sizeof(int)
的间隔向上跳跃。
如果没有通过可靠的测试和对依赖于平台的优化 asm 的仔细检查来进行基准测试,你们两个人都没有。如果花时间这样做,您可能双方都会感到惊讶。
这取决于平台,但在大多数平台上,这两种变体可能同样有效。看编译器生成的汇编代码,自己看。
也很难想象这种级别的性能会很重要。我见过太多的代码,人们在进入 O(n^2) 算法的过程中从堆栈中删除了几个字节,该算法应该是 O(n) 或 O(log n)...
【参考方案1】:
如果函数是在同一个翻译单元中定义的(并且原型只是一个前向声明),那么没关系,编译器很可能会内联该函数,而您将无法区分。
如果函数是在另一个翻译单元(外部链接)中定义的,那么编译器会生成一个函数调用。大多数调用约定在寄存器中传递前几个参数,对于字符或对字符的引用肯定是这种情况。如果按值传递,编译器会将字符加载到第一个参数的寄存器中,如果按引用传递,编译器会将字符的地址放在第一个参数的寄存器中,然后调用的函数将从那个地址。哪个更有效率?可能是按值传递,但在当今的 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
【讨论】:
【参考方案2】:事实上,您无法预测优化后会是什么样子;唯一保持“固定”的是代码的语义,而不是它的实际执行方式。
【讨论】:
【参考方案3】:你俩都错了。
引用需要指向原始对象的指针。不是整数。可能是 64 位。
一个 char 被压入堆栈,而不是复制到内存中的其他地方,并且使用标准打包这可能就像一个 64 位的 int。
有问题的指针必须在稍后得到引用,以获取引用的值,在大多数硬件上拉入整个 64 字节的缓存行,如果它还没有从调用缓存中。您需要拉入相同的缓存行以将其推送到堆栈上,因此差别很小。但是如果 char 存储在寄存器中,那么它可能已经被压入堆栈而没有读入缓存行。
如果您对速度进行了优化,如果它不是参考,它可能会保留在同一个寄存器中。聪明的编译器家伙可能会看到你做了一些愚蠢的事情,比如通过 const 引用传递一个 pod 类型并将其保存在寄存器中以使你看起来不错,但你不应该总是依赖编译器让你看起来不错。
除非您担心有人可能会意外更改此函数中 char 的值,否则为什么要将其作为 const ref 传递?
每个编译器/平台都不同,有时引用可能会花费更多,但对于 pod 类型,传递我的值永远不会比引用花费更多。
所以是的,你都错了。
【讨论】:
我不同意在所有情况下都按值传递 POD。如果用户定义的 POD 比一个指针(或几个指针)大,它通常应该作为 const ref 传递。您可能已经记住了内置类型(它们构成了所有 POD 的子集)。没有例外:在 8 位或 16 位控制器上,通过例如const ref 的 double (堆栈非常宝贵,内存副本非常昂贵)。 如果它的用户定义,那么它不是一个 POD。 POD 表示普通 Ol 数据。 chars、short、int、float、double 和 QWORD.. 不是用户定义的类型。 @Dan,POD 不仅仅是内置的基本类型。 ***.com/questions/146452/what-are-pod-types-in-c。侮辱人也不好。也是“你是”而不是“你的”。【参考方案4】:这是当您通过引用传递时在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 个字节。
将作为x
的副本的c
放入堆栈中,假设内存地址为B
。它是 1 个字节。
使用c
时,其内存位置为[dereference B]
。
返回[dereference A]
。
地址A
和B
(相对于栈顶)被硬编码到编译器生成的二进制可执行文件中。
第 2 步和第 3 步的区别在于大小和单或双解引用,两者都支持按值传递。
也就是说,启用优化的现代编译器可能会通过内联函数来优化上述两个程序——如果足够简单,会产生以下结果:
-
---
---
当使用
c
作为fun
的参数时,将使用x
。
---
【讨论】:
【参考方案5】:你们都错了。没有 1 字节的堆栈槽。但是当你通过引用传递一个字符时:
-
你必须计算它的地址。如果它是静态的,那就是恒定的。如果它在对象中,则必须在对象的地址上添加偏移量。如果它在堆栈上,方法本地,您必须将其堆栈帧偏移添加到当前堆栈帧指针。在 x86 上使用 LEA 指令完成,但您使用的是英特尔硬件吗?
然后您必须推送地址。
然后,每次在目标方法中使用它时,都必须取消引用它。
所有这些都是内存引用,而不仅仅是将值压入堆栈。
在非平凡的方法中是否真的很重要是另一个问题。当然,在某些情况下,编译器也可以将其编译为传递引用。
【讨论】:
以上是关于在 C++ 中通过引用传递 char 的效率的主要内容,如果未能解决你的问题,请参考以下文章