C + + 视图类型: 通过 const & 还是通过值传递?

这在最近的代码审查讨论中出现过,但是没有得到一个令人满意的结论。所讨论的类型类似于 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 类型)。

支持按值传递的参数相当于“少输入”,“如果视图有有意义的变化,则可以对本地副本进行变异”,以及“可能不会降低效率”。

支持通过 const 引用传递对象的参数相当于“更习惯于通过 const & 传递对象”,而且“效率可能不会降低”。

在通过值或常量引用传递惯用视图类型是否更好的问题上,是否还有其他考虑因素可能使参数产生决定性的变化。

对于这个问题,可以安全地假设 C + + 11或 C + + 14语义,以及足够现代的工具链和目标体系结构等。

14067 次浏览

下面是我给函数传递变量的经验法则:

  1. 如果变量能够适应处理器的寄存器而不能 被修改,通过值传递。
  2. 如果要修改变量,则按引用传递。
  3. 如果变量大于处理器的寄存器并且不会 被修改,通过不断参考。
  4. 如果需要使用指针,请通过智能指针传递。

希望能帮上忙。

值是值,常量引用是常量引用。

如果对象不是不可变的,那么这两个就是 没有等价的概念。

是的... 甚至通过 const引用接收到的对象也可能发生变异(甚至可能在您手中仍然有一个 const 引用时被销毁)。带有引用的 const只说明 用那个引用可以做什么,它没有说明被引用的对象不会变异或不会通过其他方式停止存在。

要查看一个非常简单的例子,其中别名可以严重咬伤明显合法的代码参见 这个答案

您应该在逻辑需要引用的地方使用引用(即对象 身份很重要)。当逻辑只需要一个值时,您应该传递一个值(也就是说,对象 身份是不相关的)。具有不可变性时,身份通常是不相关的。

当你使用参考特别注意应支付别名和终身问题。另一方面,当传递值时,你应该考虑到可能涉及到复制,因此,如果类很大,这可能是你的程序的一个严重瓶颈,那么你可以考虑传递一个常量引用(和双重检查别名和生存期问题)。

在我看来,在这个特定的情况下(仅仅是两个本机类型) ,需要常量引用传递效率的借口是很难证明的。大多数情况下,所有东西都是内联的,引用只会让事情更难优化。

如果被调用方对标识不感兴趣(即将来的 *状态更改) ,则指定 const T&参数是一个设计错误。故意发生这种错误的唯一理由是对象很重,而复制副本是一个严重的性能问题。

对于小对象,从性能角度来看,复制通常是 好多了,因为只有一个间接对象,而且偏执的优化器不需要考虑别名问题。例如,如果 F(const X& a, Y& b)X包含 Y类型的成员,优化器将被迫考虑非常量引用实际绑定到 X的子对象的可能性。

(*)使用“ future”,我包括从方法返回(即被调用方存储对象的地址并记住它)以及在执行被调用方代码(即别名)期间。

因为在这种情况下,你用哪一个并没有丝毫的不同,这似乎只是一场关于自我的辩论。这不应该成为代码审查的障碍。除非有人测量了性能,并且发现这段代码对时间非常重要,这一点我非常怀疑。

当有疑问时,按价值传递。

现在,你应该很少怀疑。

通常,价值观的传递代价高昂,而带来的好处却微乎其微。有时,您实际上希望引用存储在其他地方的可能发生变化的值。通常,在泛型代码中,您不知道复制是否是一个昂贵的操作,因此错误地选择了不复制。

当有疑问时,应该通过值传递的原因是因为值更容易推理。对外部数据的引用(甚至是 const引用)可能会在调用函数回调或其他方法时在算法中发生变异,从而将看似简单的函数变成复杂的混乱。

在这种情况下,您已经有了一个隐式引用绑定(指向您正在查看的容器的内容)。添加另一个隐式引用绑定(到查看容器的视图对象中)也同样糟糕,因为已经存在并发症。

最后,编译器可以更好地推理值,而不是推理对值的引用。如果你离开了本地分析范围(通过一个函数指针回调) ,编译器必须假定 const 引用中存储的值可能已经完全改变了(如果它不能证明相反的话)。自动存储中的一个值,如果没有人使用指针指向它,可以假定它不会以类似的方式进行修改——没有定义的方法来访问它并从外部作用域更改它,因此可以假定这种修改不会发生。

当你有机会将一个值作为一个值来传递时,就要拥抱这种简单性,因为这种情况很少发生。

我的观点是两者兼用。选择 const & 。它还将成为文档。如果您已经将它声明为 const & ,那么如果您试图修改实例(当您不打算修改时) ,编译器将发出抱怨。如果你确实打算修改它,那么就按价值取用它。但是通过这种方式,您可以显式地与未来的开发人员通信,告诉他们您打算修改实例。Const & “可能并不比值差”,而且可能好得多(如果构造实例代价很高,而且您还没有实例)。

撇开关于常量和值与值的信令值作为函数参数的哲学问题不谈,我们可以看一看 ABI 在各种体系结构中的一些含义。

Http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/列出了一些 QT 人员在 x86-64、 ARMv7 hard-float、 MIPS hard-float (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限制的情况下,通过值传递可以提高性能,降低认知负荷。

那么我假设我综合了两个支持通过值传递的论点:

  1. 32位平台通常缺乏通过寄存器传递两个字结构的能力。这似乎不再是一个问题。
  2. 常量引用在数量和质量上都比值差,因为它们可以别名。

所有这些都使我倾向于对整数类型的 < 16字节结构按值传递。显然,您的收获可能会有所不同,测试应该始终在性能存在问题的地方进行,但是对于非常小的类型,值似乎更好一些。

编辑: 代码可在这里: https://github.com/acmorrow/stringview_param

我已经创建了一些示例代码,这些代码似乎证明了类似 string _ view 对象的通过值传递可以为调用方和函数定义 至少在一个站台上提供更好的代码。

首先,我们在 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函数是对编译器不了解的函数(例如来自其他动态对象的函数,等等)的任意调用的替身。声明在 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 编译成单独的动态库。编译器在 OS X Yosemite 10.10.1上编写了 XCode 6 clang:

Clang + +-mmacosx-version-min = 10.10—— stdlib = libc + +-O3-flto-March = ative- 可见性-隐藏-可见性 = 隐藏—— std = c + + 11./do _ something _ else.cpp-fPIC-share-o libdo _ something _ else. dylib Clang + +-mmacosx-version-min = 10.10—— stdlib = libc + +-O3-flto-March = ative- 可见性-隐藏-可见性 = 隐藏—— std = c + + 11./example.cpp-fPIC-share-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)

有趣的是,副值版本缩短了几条指令。但那只是函数体。那么调用者呢?

我们将定义一些函数来调用这两个重载,在 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)

而且,通过值的版本也缩短了几个指令。

在我看来,至少从指令计数的粗略度量来看,按值版本为调用者和生成的函数体生成了更好的代码。

我当然愿意接受关于如何改进这个测试的建议。显然,下一步是将其重构为我可以对其进行有意义的基准测试的内容。我会尽快的。

我将使用某种构建脚本将示例代码发布到 github,这样其他人就可以在他们的系统上进行测试。

但是基于上面的讨论以及检查生成代码的结果,我的结论是通过值传递是获得视图类型的方法。

除了这里已经说过的有利于通过值传递的内容之外,现代的 C + + 优化器还要努力解决参考参数的问题。

当被调用方的主体在翻译单元中不可用时(函数驻留在一个共享库或另一个翻译单元中,链接时间优化不可用) ,会发生以下情况:

  1. 优化器假设通过引用或引用传递给 const 的参数可以更改(因为 const_castconst并不重要) ,或者由全局指针引用,或者由另一个线程更改。基本上,通过引用传递的参数成为调用站点中的 被下毒了值,优化器不能再对其应用许多优化。
  2. 在被调用方中,如果有几个相同基类型的引用/指针参数,优化器会假设它们与其他参数别名,这又会排除许多优化。
  3. 此外,所有 char类型数组都可以为任何其他类型的值提供别名,因此修改任何 std::string对象都意味着修改任何和所有其他对象,从而导致以下机器代码必须从内存中重新加载所有对象。在 C中加入了 restrict关键字,正好解决了这个问题。不同的地址可能仍然是别名,因为一个人可以将一个页面帧多次映射到一个虚拟地址空间(这是一个流行的技巧,为0-拷贝接收环缓冲区) ,这就是为什么编译器不能假定不同的地址没有别名,除非使用 restrict关键字。

从优化器的角度来看,通过值传递和返回的视图是最好的,因为这避免了别名分析的需要: 调用方和被调用方拥有它们的值副本,这样就不能从其他地方修改这些值。

对于一个主题的详细处理,我不能推荐足够的 Chandler Carruth: C + + 涌现结构的优化。演讲的要点是“人们需要改变对传递价值的看法... 传递参数的寄存器模型已经过时了。”