提问人:acm 提问时间:12/3/2014 最后编辑:acm 更新时间:2/15/2022 访问量:15974
C++ 视图类型:按 const& 传递还是按值传递?
C++ view types: pass by const& or by value?
问:
这在最近一次代码审查讨论中出现,但没有一个令人满意的结论。所讨论的类型是 C++ string_view TS 的类似物。它们是围绕指针和长度的简单非所有权包装器,用一些自定义函数进行装饰:
#include <cstddef>
class foo_view {
public:
foo_view(const char* data, std::size_t len)
: _data(data)
, _len(len) {
}
// member functions related to viewing the 'foo' pointed to by '_data'.
private:
const char* _data;
std::size_t _len;
};
问题在于,无论哪种方式,是否都倾向于通过值或常量引用来传递此类视图类型(包括即将到来的 string_view 和 array_view 类型)。
支持按值传递的论点相当于“减少键入”、“如果视图有有意义的突变,可以改变本地副本”,以及“可能效率不低”。
支持通过常量引用的论点相当于“通过常量传递对象更习惯”,并且“可能效率不低”。
是否有任何其他考虑因素可能会以一种或另一种方式最终摆动论点,即按值或常量引用传递惯用视图类型是否更好。
对于这个问题,可以安全地假设 C++11 或 C++14 语义,以及足够现代的工具链和目标架构等。
答:
以下是我将变量传递给函数的经验法则:
- 如果变量可以放入处理器的寄存器中,并且不会 被修改,按值传递。
- 如果要修改变量,请按引用传递。
- 如果变量大于处理器的寄存器,并且不会 被修改,通过常量引用传递。
- 如果需要使用指针,请传递智能指针。
希望能有所帮助。
评论
值是值,常量引用是常量引用。
如果对象不是不可变的,那么这两者就不是等价的概念。
是的。。。即使是通过引用接收的对象也可以变异(甚至可以在你手中仍然有一个常量引用时被破坏)。 引用仅说明使用该引用可以做什么,它没有说明引用对象不会变异或不会通过其他方式停止存在。const
const
要查看一个非常简单的情况,其中混叠可能会严重影响看似合法的代码,请参阅此答案。
您应该在逻辑需要引用的地方使用引用(即对象标识很重要)。当逻辑只需要一个值时,你应该传递一个值(即对象标识是无关紧要的)。对于不可变性,通常身份是无关紧要的。
使用引用时,应特别注意混叠和生存期问题。另一方面,在传递值时,您应该考虑可能涉及复制,因此,如果类很大并且这被证明是程序的严重瓶颈,那么您可以考虑传递常量引用(并仔细检查别名和生存期问题)。
在我看来,在这种特定情况下(只有几种原生类型),需要常量引用传递效率的借口很难证明是合理的。最有可能的是,无论如何,一切都会被内联,而引用只会使事情更难优化。
当被调用方对标识不感兴趣时指定参数(即未来*状态更改)是设计错误。故意犯此错误的唯一理由是,当对象很重并且进行复制是一个严重的性能问题时。const T&
对于小对象,从性能的角度来看,制作副本实际上通常更好,因为少了一个间接,而且优化器偏执的一面不需要考虑混叠问题。例如,如果具有并包含 类型的成员,则优化器将被迫考虑非 const 引用实际上绑定到 的子对象的可能性。F(const X& a, Y& b)
X
Y
X
(*)对于“future”,我在从方法返回后(即被调用方存储对象的地址并记住它)和在被调用方代码执行期间(即别名)都包括在内。
评论
const T&
T
F(const T&)
F
F(T)
rect
-=(const P2d&)
const T&
F(const X& a, X& b)
由于在这种情况下使用哪一个没有丝毫区别,这似乎只是一场关于自我的辩论。这不应该阻碍代码审查。除非有人测量性能并发现这段代码对时间至关重要,否则我非常非常怀疑。
评论
如有疑问,请按值传递。
现在,你应该很少怀疑。
通常,通过价值的成本很高,而且几乎没有好处。有时,您实际上希望引用存储在其他地方的可能发生变异的值。通常,在通用代码中,您不知道复制是否是一项代价高昂的操作,因此您犯了错误。
有疑问时应该按值传递的原因是,值更容易推理。当您调用函数回调或您拥有的东西时,对外部数据的引用(即使是一个)可能会在算法中间发生变化,从而将看似简单的函数变成复杂的混乱。const
在本例中,您已经有一个隐式引用绑定(到您正在查看的容器的内容)。添加另一个隐式引用绑定(到查看容器的视图对象)同样糟糕,因为已经存在复杂性。
最后,编译器可以更好地推理值,而不是对值的引用。如果离开本地分析的范围(通过函数指针回调),编译器必须假定存储在常量引用中的值可能已完全更改(如果它无法证明相反)。在自动存储中,没有人接受指向它的指针的值,可以假定它不会以类似的方式进行修改 -- 没有定义的方式来访问它并从外部作用域更改它,因此可以假定不会发生此类修改。
当您有机会将值作为值传递时,请拥抱简单性。这种情况很少发生。
评论
我的论点是两者兼而有之。首选 const&。它也可以是文档。如果您已将其声明为 const&,则如果您尝试修改实例(当您不打算修改时),编译器将抱怨。如果您确实打算修改它,请按值获取它。但这样一来,您就明确地向未来的开发人员传达了您打算修改实例的信息。const& “可能不比价值差”,而且可能更好(如果构建一个实例很昂贵,而你还没有一个实例)。
评论
const
撇开关于常量与值作为函数参数的信号值的哲学问题不谈,我们可以看看 ABI 对各种架构的一些影响。
http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/ 列出了一些QT人员在x86-64、ARMv7硬浮点、MIPS硬浮点(o32)和IA-64上所做的一些决策和测试。大多数情况下,它检查函数是否可以通过寄存器传递各种结构。毫不奇怪,似乎每个平台都可以通过寄存器管理 2 个指针。鉴于 sizeof(size_t) 通常是 sizeof(void*),因此没有理由相信我们会在这里溢出内存。
我们可以找到更多的柴火,考虑以下建议:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3538.html。请注意,const ref 有一些缺点,即混叠的风险,这可能会阻止重要的优化,并且需要程序员进行额外的思考。在没有 C++ 支持 C99 限制的情况下,按值传递可以提高性能并降低认知负荷。
我想我正在综合两个支持按值传递的论点:
- 32 位平台通常缺乏通过寄存器传递两个字结构的能力。这似乎不再是一个问题。
- 常量引用在数量和质量上都比值差,因为它们可以别名。
所有这些都会导致我赞成整数类型的 <16 字节结构的按值传递。显然,您的里程可能会有所不同,并且应始终在性能有问题的情况下进行测试,但对于非常小的类型,数值似乎确实更好一些。
评论
编辑:代码可在此处获得:https://github.com/acmorrow/stringview_param
我创建了一些示例代码,这些代码似乎表明,对类似对象string_view进行按值传递可以为至少一个平台上的调用者和函数定义提供更好的代码。
首先,我们在以下位置定义一个假string_view类(我手头没有真实的东西):string_view.h
#pragma once
#include <string>
class string_view {
public:
string_view()
: _data(nullptr)
, _len(0) {
}
string_view(const char* data)
: _data(data)
, _len(strlen(data)) {
}
string_view(const std::string& data)
: _data(data.data())
, _len(data.length()) {
}
const char* data() const {
return _data;
}
std::size_t len() const {
return _len;
}
private:
const char* _data;
size_t _len;
};
现在,让我们定义一些使用string_view的函数,无论是按值还是按引用。以下是以下签名:example.hpp
#pragma once
class string_view;
void __attribute__((visibility("default"))) use_as_value(string_view view);
void __attribute__((visibility("default"))) use_as_const_ref(const string_view& view);
这些函数的主体定义如下:example.cpp
#include "example.hpp"
#include <cstdio>
#include "do_something_else.hpp"
#include "string_view.hpp"
void use_as_value(string_view view) {
printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
do_something_else();
printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}
void use_as_const_ref(const string_view& view) {
printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
do_something_else();
printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}
这里的函数是对编译器无法了解的函数的任意调用的替身(例如,来自其他动态对象的函数等)。声明在:do_something_else
do_something_else.hpp
#pragma once
void __attribute__((visibility("default"))) do_something_else();
而微不足道的定义是:do_something_else.cpp
#include "do_something_else.hpp"
#include <cstdio>
void do_something_else() {
std::printf("Doing something\n");
}
现在,我们将do_something_else.cpp和示例.cpp编译到单独的动态库中。编译器是 OS X Yosemite 6 上的 XCode 10.10.1:
clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./do_something_else.cpp -fPIC -shared -o libdo_something_else.dylib
clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example.cpp -fPIC -shared -o libexample.dylib -L. -ldo_something_else
现在,我们反汇编 libexample.dylib:
> otool -tVq ./libexample.dylib
./libexample.dylib:
(__TEXT,__text) section
__Z12use_as_value11string_view:
0000000000000d80 pushq %rbp
0000000000000d81 movq %rsp, %rbp
0000000000000d84 pushq %r15
0000000000000d86 pushq %r14
0000000000000d88 pushq %r12
0000000000000d8a pushq %rbx
0000000000000d8b movq %rsi, %r14
0000000000000d8e movq %rdi, %rbx
0000000000000d91 movl $0x61, %esi
0000000000000d96 callq 0xf42 ## symbol stub for: _strchr
0000000000000d9b movq %rax, %r15
0000000000000d9e subq %rbx, %r15
0000000000000da1 movq %rbx, %rdi
0000000000000da4 callq 0xf48 ## symbol stub for: _strlen
0000000000000da9 movq %rax, %rcx
0000000000000dac leaq 0x1d5(%rip), %r12 ## literal pool for: "%ld %ld %zu\n"
0000000000000db3 xorl %eax, %eax
0000000000000db5 movq %r12, %rdi
0000000000000db8 movq %r15, %rsi
0000000000000dbb movq %r14, %rdx
0000000000000dbe callq 0xf3c ## symbol stub for: _printf
0000000000000dc3 callq 0xf36 ## symbol stub for: __Z17do_something_elsev
0000000000000dc8 movl $0x61, %esi
0000000000000dcd movq %rbx, %rdi
0000000000000dd0 callq 0xf42 ## symbol stub for: _strchr
0000000000000dd5 movq %rax, %r15
0000000000000dd8 subq %rbx, %r15
0000000000000ddb movq %rbx, %rdi
0000000000000dde callq 0xf48 ## symbol stub for: _strlen
0000000000000de3 movq %rax, %rcx
0000000000000de6 xorl %eax, %eax
0000000000000de8 movq %r12, %rdi
0000000000000deb movq %r15, %rsi
0000000000000dee movq %r14, %rdx
0000000000000df1 popq %rbx
0000000000000df2 popq %r12
0000000000000df4 popq %r14
0000000000000df6 popq %r15
0000000000000df8 popq %rbp
0000000000000df9 jmp 0xf3c ## symbol stub for: _printf
0000000000000dfe nop
__Z16use_as_const_refRK11string_view:
0000000000000e00 pushq %rbp
0000000000000e01 movq %rsp, %rbp
0000000000000e04 pushq %r15
0000000000000e06 pushq %r14
0000000000000e08 pushq %r13
0000000000000e0a pushq %r12
0000000000000e0c pushq %rbx
0000000000000e0d pushq %rax
0000000000000e0e movq %rdi, %r14
0000000000000e11 movq (%r14), %rbx
0000000000000e14 movl $0x61, %esi
0000000000000e19 movq %rbx, %rdi
0000000000000e1c callq 0xf42 ## symbol stub for: _strchr
0000000000000e21 movq %rax, %r15
0000000000000e24 subq %rbx, %r15
0000000000000e27 movq 0x8(%r14), %r12
0000000000000e2b movq %rbx, %rdi
0000000000000e2e callq 0xf48 ## symbol stub for: _strlen
0000000000000e33 movq %rax, %rcx
0000000000000e36 leaq 0x14b(%rip), %r13 ## literal pool for: "%ld %ld %zu\n"
0000000000000e3d xorl %eax, %eax
0000000000000e3f movq %r13, %rdi
0000000000000e42 movq %r15, %rsi
0000000000000e45 movq %r12, %rdx
0000000000000e48 callq 0xf3c ## symbol stub for: _printf
0000000000000e4d callq 0xf36 ## symbol stub for: __Z17do_something_elsev
0000000000000e52 movq (%r14), %rbx
0000000000000e55 movl $0x61, %esi
0000000000000e5a movq %rbx, %rdi
0000000000000e5d callq 0xf42 ## symbol stub for: _strchr
0000000000000e62 movq %rax, %r15
0000000000000e65 subq %rbx, %r15
0000000000000e68 movq 0x8(%r14), %r14
0000000000000e6c movq %rbx, %rdi
0000000000000e6f callq 0xf48 ## symbol stub for: _strlen
0000000000000e74 movq %rax, %rcx
0000000000000e77 xorl %eax, %eax
0000000000000e79 movq %r13, %rdi
0000000000000e7c movq %r15, %rsi
0000000000000e7f movq %r14, %rdx
0000000000000e82 addq $0x8, %rsp
0000000000000e86 popq %rbx
0000000000000e87 popq %r12
0000000000000e89 popq %r13
0000000000000e8b popq %r14
0000000000000e8d popq %r15
0000000000000e8f popq %rbp
0000000000000e90 jmp 0xf3c ## symbol stub for: _printf
0000000000000e95 nopw %cs:(%rax,%rax)
有趣的是,按值版本短了几条指令。但这只是函数体。来电者呢?
我们将定义一些调用这两个重载的函数,在 :const std::string&
example_users.hpp
#pragma once
#include <string>
void __attribute__((visibility("default"))) forward_to_use_as_value(const std::string& str);
void __attribute__((visibility("default"))) forward_to_use_as_const_ref(const std::string& str);
并在以下位置定义它们:example_users.cpp
#include "example_users.hpp"
#include "example.hpp"
#include "string_view.hpp"
void forward_to_use_as_value(const std::string& str) {
use_as_value(str);
}
void forward_to_use_as_const_ref(const std::string& str) {
use_as_const_ref(str);
}
同样,我们编译为一个共享库:example_users.cpp
clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example_users.cpp -fPIC -shared -o libexample_users.dylib -L. -lexample
再一次,我们看一下生成的代码:
> otool -tVq ./libexample_users.dylib
./libexample_users.dylib:
(__TEXT,__text) section
__Z23forward_to_use_as_valueRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000e70 pushq %rbp
0000000000000e71 movq %rsp, %rbp
0000000000000e74 movzbl (%rdi), %esi
0000000000000e77 testb $0x1, %sil
0000000000000e7b je 0xe8b
0000000000000e7d movq 0x8(%rdi), %rsi
0000000000000e81 movq 0x10(%rdi), %rdi
0000000000000e85 popq %rbp
0000000000000e86 jmp 0xf60 ## symbol stub for: __Z12use_as_value11string_view
0000000000000e8b incq %rdi
0000000000000e8e shrq %rsi
0000000000000e91 popq %rbp
0000000000000e92 jmp 0xf60 ## symbol stub for: __Z12use_as_value11string_view
0000000000000e97 nopw (%rax,%rax)
__Z27forward_to_use_as_const_refRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000ea0 pushq %rbp
0000000000000ea1 movq %rsp, %rbp
0000000000000ea4 subq $0x10, %rsp
0000000000000ea8 movzbl (%rdi), %eax
0000000000000eab testb $0x1, %al
0000000000000ead je 0xebd
0000000000000eaf movq 0x10(%rdi), %rax
0000000000000eb3 movq %rax, -0x10(%rbp)
0000000000000eb7 movq 0x8(%rdi), %rax
0000000000000ebb jmp 0xec7
0000000000000ebd incq %rdi
0000000000000ec0 movq %rdi, -0x10(%rbp)
0000000000000ec4 shrq %rax
0000000000000ec7 movq %rax, -0x8(%rbp)
0000000000000ecb leaq -0x10(%rbp), %rdi
0000000000000ecf callq 0xf66 ## symbol stub for: __Z16use_as_const_refRK11string_view
0000000000000ed4 addq $0x10, %rsp
0000000000000ed8 popq %rbp
0000000000000ed9 retq
0000000000000eda nopw (%rax,%rax)
而且,同样,按值版本短了几条指令。
在我看来,至少从指令计数的粗略指标来看,按值版本为调用方和生成的函数体生成更好的代码。
我当然愿意接受关于如何改进这个测试的建议。显然,下一步是将其重构为可以有意义地对其进行基准测试的东西。我会尽快尝试这样做。
我将使用某种构建脚本将示例代码发布到 github,以便其他人可以在他们的系统上进行测试。
但基于上面的讨论,以及检查生成的代码的结果,我的结论是,按值传递是视图类型的必经之路。
评论
除了这里已经说过的赞成按值传递的内容之外,现代 C++ 优化器还在与引用参数作斗争。
当被调用方的正文在翻译单元中不可用时(该函数驻留在共享库或其他翻译单元中,并且链接时间优化不可用),则会发生以下情况:
- 优化器假定通过引用或引用传递给 const 的参数可以更改( 无关紧要,因为 ) 或由全局指针引用,或由另一个线程更改。基本上,通过引用传递的参数会成为调用站点中的中毒值,优化程序无法再对其应用许多优化。
const
const_cast
- 在被调用方中,如果存在多个相同基类型的引用/指针参数,则优化器会假定它们与其他参数别名,这再次排除了许多优化。
- 此外,所有类型数组都可以为任何其他类型的值添加别名,因此修改任何对象都意味着修改任何其他对象,导致以下机器代码必须从内存中重新加载所有对象。 添加了关键字,以解决这种低效率问题。不同的地址可能仍然是别名,因为可以将一个页面帧多次映射到一个虚拟地址空间中(这是 0 副本接收环缓冲区的流行技巧),这就是为什么编译器不能假设不同地址没有别名,除非使用关键字。
char
std::string
restrict
C
restrict
从优化器的角度来看,按值传递和返回是最好的,因为这样就不需要别名分析:调用方和被调用方完全拥有其值副本,因此这些值无法从其他任何地方修改。
对于这个主题的详细处理,我不能推荐足够的钱德勒·卡鲁斯:优化C++的涌现结构。演讲的重点在于“人们需要改变对价值传递的看法......传递参数的寄存器模型已经过时了。
上一个:按值传递的速度要快得多吗?
评论