C ++视图types:通过const&或通过值?
这是最近在代码审查讨论中提出的,但没有得到令人满意的结论。 所讨论的types与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; };
问题出现在于是否有一个参数可以通过值或const引用传递这种视图types(包括即将到来的string_view和array_viewtypes)。
赞成按值传递的论据等于“较less打字”,“如果视图具有有意义的突变,则可以突变本地副本”,以及“可能效率不高”。
支持传递const引用的论据相当于“更习惯于通过const&传递对象”和“可能效率不高”。
是否有任何额外的考虑可能以某种方式决定性地摆动论点,或者根据价值或常量引用来传递惯用视图types。
对于这个问题,假定C ++ 11或C ++ 14语义以及足够现代的工具链和目标体系结构是安全的。
如有疑问,按价值传递。
现在,你应该很less有人怀疑。
价值通常很昂贵,没有什么好处。 有时你实际上想要引用一个可能在其他地方存储的变异值。 通常,在通用代码中,你不知道复制是否是一个昂贵的操作,所以你错在了一边。
如果有疑问,你应该通过价值传递的原因是因为价值观更容易推理。 当你调用一个函数callback函数或者你有什么函数的时候,对外部数据的引用(甚至是const
)可能会在algorithm的中间发生变化,这似乎是一个简单的函数。
在这种情况下,您已经有一个隐含的引用绑定(您正在查看的容器的内容)。 添加另一个隐式引用绑定(查看容器的视图对象)也不错,因为已经有了复杂性。
最后,编译器可以比关于值的引用更好地推理值。 如果您离开本地分析的范围(通过函数指针callback),编译器必须假定存储在const引用中的值可能已经完全改变(如果不能certificate相反的话)。 自动存储中没有人指向它的指针的值可以被假定为不以类似的方式修改 – 没有定义的方式来访问它并从外部范围改变它,所以这样的修改可以被假定为不发生。
当你有机会传递价值作为价值时,拥抱简单。 它只发生很less。
编辑:代码可在这里: https : //github.com/acmorrow/stringview_param
我创build了一些示例代码,它似乎certificate了string_view类似对象的传递值可以为至less一个平台上的调用者和函数定义生成更好的代码。
首先,我们在string_view.h中定义一个假的string_view类(我没有真正的东西):
#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
函数在这里是对编译器没有深入了解的函数的任意调用(例如来自其他dynamic对象的函数等)的替代。 声明在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和example.cpp编译成单独的dynamic库。 这里编译的是OS X Yosemite 10.10.1上的XCode 6 clang:
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)
有趣的是,按价值版本是几个指令更短。 但那只是function团体。 来电者怎么样?
我们将定义一些调用这两个重载的const std::string&
,在example_users.hpp
转发一个const std::string&
#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)
再次,价值版本是几个指令更短。
在我看来,至less通过指令计数的粗略度量,按值版本为调用者和生成的函数体产生更好的代码。
我当然愿意提出如何改进这个testing的build议。 显而易见,下一步将重构这个东西,我可以有意义的基准。 我会尽快做到这一点。
我将使用某种构build脚本将示例代码发布到github,以便其他人可以在他们的系统上进行testing。
但是根据上面的讨论和检查生成的代码的结果,我的结论是传值是视图types的path。
抛开关于常量与价值的信号值作为函数参数的哲学问题,我们可以看看在不同架构上的一些ABI含义。
http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/展示了一些QT人员在x86-64,ARMv7 hard-float, MIPS硬浮点(o32)和IA-64。 大多数情况下,它检查函数是否可以通过寄存器传递各种结构。 毫不奇怪,看来每个平台都可以通过注册来pipe理2个指针。 考虑到sizeof(size_t)通常是sizeof(void *),没有什么理由相信我们会在这里记忆。
我们可以find更多的木材,考虑如下build议: http : //www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3538.html 。 请注意,const ref有一些缺点,即混叠的风险,这可能会阻止重要的优化,并需要程序员额外的思考。 在没有C ++支持C99的限制的情况下,按值传递可以提高性能并降低认知负载。
那么我想我正在综合两个有利于价值传递的论点:
- 32位平台往往缺乏通过注册传递两个字结构的能力。 这不再是一个问题。
- const引用在数量和质量上都比可以replace的值更差。
所有这些将导致我倾向于<16字节的整数types结构的值传递。 显然,你的里程可能会有所不同,testing应该始终在性能问题上完成,但对于非常小的types,数值看起来好一些。
这里是我的经验法则传递variables的函数:
- 如果variables可以放在处理器的寄存器中,并且不会被修改,那么按值传递。
- 如果该variables将被修改,则按引用传递。
- 如果variables大于处理器的寄存器并且不被修改,则通过常量引用。
- 如果你需要使用指针,通过智能指针传递。
希望有所帮助。
现在的C ++优化器除了在这里已经说过赞成按值传递之外,还与参考论证争执不休。
当被调用者的主体在翻译单元中不可用时(函数驻留在共享库或另一个翻译单元中,并且链接时优化不可用),则会发生以下情况:
- 优化器假定通过引用或引用传递给const的参数可以被改变(
const
由const_cast
不重要),或由全局指针引用,或由另一个线程改变。 基本上,通过引用传递的参数在调用站点中变成“中毒”的值,优化器不能再应用许多优化。 - 在被调用者中,如果有几个相同基types的引用/指针参数,那么优化器会假定它们与别的别名混淆了,并且再次排除了许多优化。
从优化器的angular度来看,传递和返回值是最好的,因为这样可以避免别名分析的需要:调用者和被调用者拥有它们的值的唯一拷贝,以便这些值不能被修改。
对于这个问题的详细处理,我不能推荐足够的Chandler Carruth:优化C ++的Emergent结构 。 谈话的焦点是“人们需要改变头脑,关于价值传递……过时论证的注册模式已经过时”。
值是一个值,const引用是一个const引用。
如果对象不是不可变的,那么这两个不是等价的概念。
是的,即使是通过const
引用接收到的对象也可以进行变异(甚至可以在你手中仍然有const引用时被销毁)。 只有一个引用const
说什么可以使用该引用 ,它没有提到任何关于被引用的对象不会改变或不会停止存在的其他手段。
要看到一个非常简单的情况,其中别名可以咬合显然合法的代码看到这个答案 。
您应该在逻辑需要引用的地方使用引用(即对象标识很重要)。 当逻辑需要值时,你应该传递一个值(即对象标识是不相关的)。 通常不变的身份是不相关的。
当您使用参考时,应特别注意锯齿和寿命问题。 另一方面,当传递值的时候,你应该考虑复制是可能的,因此如果这个类很大,这对于你的程序来说可能是一个严重的瓶颈,那么你可以考虑传递一个const引用(并且仔细检查别名和生命期问题) 。
在我看来,在这个特定的情况下(仅仅是几种本地types),需要const引用传递效率的借口是很难certificate的。 无论如何,很可能一切都将被内联,引用只会使事情难以优化。
当被调用者对身份不感兴趣时(即将来的状态改变),指定一个const T&
参数是一个devise错误。 有意识地犯这个错误的唯一理由是当对象很重并且复制是一个严重的性能问题时。
对于小对象来说,从性能angular度来看,复制通常实际上更好 ,因为有一个间接性较less,优化器偏执侧不需要考虑混叠问题。 例如,如果你有F(const X& a, Y& b)
并且X
包含Y
types的成员,优化器将被迫考虑非const引用实际绑定到X
子对象的可能性。
(*)对于“未来”,我在从方法返回之后(即被调用者存储对象的地址并记住它)和执行被调用代码(即混叠)期间都包括这两者。
既然在这种情况下使用哪一个并没有丝毫的区别,这似乎只是一个关于自我的辩论。 这不应该阻止代码审查。 除非有人衡量这个performance,并且认为这个代码是时间关键的,我非常怀疑。
我的理由是使用两者。 喜欢const&。 它也成为文档。 如果你已经把它声明为一个const&,那么如果你试图修改这个实例(当你不打算这样做的话),编译器会报错。 如果你打算修改它,然后把它拿下来。 但是这样你就可以明确地向未来的开发者传达你打算修改这个实例。 而const&可能“不会更糟糕”,而且可能好得多(如果构build一个实例是昂贵的,而且你还没有一个)。