c++中函数参数里,是否能用 const reference 的地方尽量都用?

您所在的位置:网站首页 内联函数在编译时是否做参数类型检查 c++中函数参数里,是否能用 const reference 的地方尽量都用?

c++中函数参数里,是否能用 const reference 的地方尽量都用?

2023-04-08 18:45| 来源: 网络整理| 查看: 265

当参数类型可确定时,对于基本类型和拷贝成本很低的类型,按值传递应该是首选。基本类型不用多说,对于类似 string_view 的可平凡复制构造类型,在现代编译器中有一项重要的优化叫 Scalar Replacement of Aggregates (SRA / SROA),这种优化可以直接把类的成员使用寄存器传入函数,这里如果按引用传参的话,编译器可能会因为受到 ABI 的约束,不进行类似的优化

void foo_0(const string_view &x) { std::printf("%s\n", x.data()); } void foo_1(const string_view x) { std::printf("%s\n", x.data()); }

对于以上代码,编译器产生汇编如下

foo_0: pushq %rbp movq %rsp, %rbp movq (%rdi), %rdi popq %rbp jmp _puts foo_1: pushq %rbp movq %rsp, %rbp popq %rbp jmp _puts

可以看出,对于值传递的 foo_1,全部操作都在寄存器中进行,但是对于 foo_0,还是需要去访问内存,取出 x.data()

值得注意的是,虽然这个优化名字叫 Scalar Replacement of Aggregates。但是此 Aggregate 并非 C++ 中的聚合类,这里的 Aggregate 要求要比聚合类松得多,不严谨的说就是可平凡复制构造类型 std::is_trivially_copy_constructible

那么对于类型不能确定的模板函数呢?诚然,我们可以写出如下代码来区分需要值传递的参数和需要引用传递的参数

template concept trivially_copy_constructible = std::is_trivially_copy_constructible_v; void foo(const trivially_copy_constructible auto x) { ... } void foo(const auto &x) { ... }

但这种方法的缺点也非常明显,对于参数数量过多的函数,这样一个一个判断需要写大量重复代码。这时候还是得靠编译器的 Scalar Replacement of Aggregates 优化。

在现代编译器中,当函数满足一些条件时(基本约等于可以内联),Scalar Replacement of Aggregates 还可以把按引用传递的参数优化为按值传递,然后在后续的内联优化中直接把函数内联了

void foo(const auto &x) { std::printf("%s\n", x.data()); } int main(int argc, char **agrv) { std::string_view x{"fuck"}; if (argc == 10) x = "shit"; foo(x); }

生成汇编为

_main: ## @main .cfi_startproc ## %bb.0: pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp cmpl $10, %edi leaq L_.str.1(%rip), %rax leaq L_.str(%rip), %rdi cmoveq %rax, %rdi callq _puts xorl %eax, %eax popq %rbp retq .cfi_endproc ## -- End function .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "fuck" L_.str.1: ## @.str.1 .asciz "shit"

直接内联优化了。

但是要注意的是,这种优化对函数要求比较多,比如上面的 foo_0函数,因为其默认具有外部链接,编译器为了遵循 ABI 要求,就没有对其做优化。

综上所述,当类型可以确定的时候,基本类型和拷贝成本很低的类型尽量还是使用按值传递;不能确定类型的时候不是对性能特别敏感的代码可以一律引用传参,实在需要优化再考虑使用 concept 或者 SFINAE分别处理。这里是因为不是所有的模板函数都是可以被内联的,比如有复杂递归的函数

static inline int ack(const auto &x, const auto &y) { int m = x, n = y; while (m != 0) { if (n == 0) n = 1; else { n = ack(m, n - 1); } m--; } return n + 1; } int main(int argc, char **agrv) { int x = 42; if (argc == 10) x = 24; std::printf("%d\n", ack(x, x)); }

生成汇编如下

ack: pushq %rbp movq %rsp, %rbp pushq %r15 pushq %r14 pushq %rbx pushq %rax movl (%rdi), %ebx movl %ebx, -28(%rbp) movl (%rsi), %eax testl %ebx, %ebx je LBB1_6 //....

可以看出,对于这种复杂的递归函数,编译器是没法进行内联的,SROA 中的引用替换也没有办法进行。

还需特别提到的是,需要小心判断函数是否可以(不是适合)使用引用传参。经典的例子如下

void foo() { auto x = std::make_shared(114514); std::thread t{[](const auto &x) { using namespace std::chrono_literals; std::this_thread::sleep_for(10ms); std::printf("%d\n", *x); }, x}; t.detach(); }

这里显然就不应该使用引用传参。

在上述的代码中其实还有一个有趣的优化,由于 std::printf("%s\n", x.data())和 std::puts(x.data()) 的效果相同,编译器直接选择了 std::puts,省去了格式化串的开销。「编译器认为你很傻,并向你展示了更高效的实现」:-P



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3