gpt4 book ai didi

c++ - 作为 `const&`轻量级对象传递

转载 作者:行者123 更新时间:2023-12-01 14:44:22 25 4
gpt4 key购买 nike

给出以下宠物片段:

template<class T1, class T2>
struct my_pair { /* constructors and such */ };

auto f(std::pair<T1, T2> const& p) // (1)
{ return my_pair<T1, T2>(p.first, p.second); }

auto f(std::pair<T1, T2> p) // (2)
{ return my_pair<T1, T2>(p.first, p.second); }

如果我知道 T1T2都是轻量级对象,它们的复制时间可以忽略不计(例如,每个指针有两个指针),那么将 std::pair作为副本传递比作为引用传递更好吗?因为我知道有时候让编译器省略副本比强制它处理引用(例如,优化复制链)更好。

同样的问题也适用于 my_pair的构造函数,如果让它们接收副本比引用更好的话。

调用上下文是未知的,但是对象生成器和类构造函数本身都是内联函数,因此,引用和值之间的差异并不重要,因为优化器可以看到最终目标并在路的尽头应用构造(I'我只是推测),因此对象生成器将是纯零开销的抽象,在这种情况下,我认为如果某些异常(exception)的配对对比平常大,则引用会更好。

但是,如果不是这种情况(即使对所有内容都是内联的,引用也总是或通常对副本有一定影响),那么我将寻求副本。

最佳答案

在微优化 Realm 之外,我通常会传递const引用,因为您没有修改对象,并且希望避免复制。如果有一天您确实使用了构造成本很高的T1T2,则该副本可能是一个大问题:传递const引用并没有同样强大的步枪。因此,我将按值传递作为具有非对称权衡的选择,并且仅在知道数据很小时才按值选择。

至于您的特定微优化问题,它基本上取决于调用是否完全内联以及您的编译器是否正确。

全内联

如果f函数的任一变体内联到调用程序中,并且启用了优化,则对于任何一个变体,您都可能获得相同或几乎相同的代码。我用inline_f_refinline_r_val调用测试here。它们都从未知的外部函数生成pair,然后调用f的按引用或变量。

对于f_val来说是这样的(f_ref版本仅在结尾更改 call ):

template <typename T>
auto inline_f_val() {
auto pair = get_pair<T>();
return f_val(pair);
}

这是 T1T2int时在gcc上的结果:
auto inline_f_ref<int>():
sub rsp, 8
call std::pair<int, int> get_pair<int>()
add rsp, 8
ret

auto inline_f_val<int>():
sub rsp, 8
call std::pair<int, int> get_pair<int>()
add rsp, 8
ret

完全相同。编译器可以正确查看这些函数,甚至可以识别 std::pairmypair实际上具有相同的布局,因此所有 f的痕迹都会消失。

这是 T1T2是带有两个指针的结构的版本:
auto inline_f_ref<twop>():
push r12
mov r12, rdi
sub rsp, 32
mov rdi, rsp
call std::pair<twop, twop> get_pair<twop>()
mov rax, QWORD PTR [rsp]
mov QWORD PTR [r12], rax
mov rax, QWORD PTR [rsp+8]
mov QWORD PTR [r12+8], rax
mov rax, QWORD PTR [rsp+16]
mov QWORD PTR [r12+16], rax
mov rax, QWORD PTR [rsp+24]
mov QWORD PTR [r12+24], rax
add rsp, 32
mov rax, r12
pop r12
ret

那是“ref”版本,再次是“val”版本。在这里,编译器无法优化所有工作:创建该对后,编译器仍然需要大量工作将 std::pair内容复制到 mypair对象(有4个存储区,总共存储32个字节,即4个指针)。因此,再次内联让编译器针对相同的事物优化版本。

您可能会发现情况并非如此,但以我的经验来看,它们并不常见。

没有内联

没有内联,这是一个不同的故事。您提到所有函数都是内联的,但这并不一定意味着编译器会内联它们。特别是gcc比内联函数更不愿意使用平均值(例如,在没有 -O2关键字的情况下,它没有在 inline中内联非常短的函数)。

如果没有内联参数传递和返回的方式,则由ABI设置,因此编译器无法优化两个版本之间的差异。 const引用版本相当于传递一个指针,因此无论 T1T2如何,您都将向第一个整数寄存器中的 std::pair对象传递一个指针。

这是在Linux上的gcc中 T1T2int时导致的代码:
auto f_ref<int, int>(std::pair<int, int> const&):
mov rax, QWORD PTR [rdi]
ret
std::pair指针在 rdi中传递,因此函数的主体是从该位置到 rax的单个8字节移动。 std::pair<int, int>占用8个字节,因此编译器可以一枪复制整个内容。在这种情况下,返回值在 rax中“按值”传递,因此我们完成了。

这取决于编译器的优化能力和ABI。例如,这是MSVC为64位Windows目标编译的同一函数:
my_pair<int,int> f_ref<int,int>(std::pair<int,int> const &) PROC ; f_ref<int,int>, COMDAT
mov eax, DWORD PTR [rdx]
mov r8d, DWORD PTR [rdx+4]
mov DWORD PTR [rcx], eax
mov rax, rcx
mov DWORD PTR [rcx+4], r8d
ret 0

这里发生了两种不同的事情。首先,ABI是不同的。 MSVC无法在 mypair<int,int>中返回 rax。而是,调用者在 rcx中传递一个指向被调用者应保存结果的位置的指针。因此,此功能除负载外还具有存储功能。 rax加载了保存数据的位置。第二件事是,编译器太笨拙,无法将两个相邻的4字节加载组合在一起并存储为8字节加载,因此有两个加载和两个存储。

第二部分可以由更好的编译器修复,但是第一部分是API的结果。

这是此功能按值的版本,在Linux上的gcc中:
auto f_val<int, int>(std::pair<int, int>):
mov rax, rdi
ret

仍然只有一条指令,但是这次只有一次reg-reg移动,这从来没有比加载更昂贵,而且通常便宜得多。

在MSVC(64位Windows)上:
my_pair<int,int> f_val<int,int>(std::pair<int,int>)
mov rax, rdx
mov DWORD PTR [rcx], edx
shr rax, 32 ; 00000020H
mov DWORD PTR [rcx+4], eax
mov rax, rcx
ret 0

您仍然有两个存储区,因为ABI仍会强制将值返回到内存中,但是由于MSVC 64位API允许将最大64位大小的参数传递到寄存器中,因此负载消失了。

然后编译器开始做一个真正愚蠢的事情:从 std::pair中的 rax的64位开始,它写出底部的32位,将顶部的32位移至底部,然后将其写出。世界上最慢的仅写出64位的方式。不过,此代码通常将比按引用版本要快。

在两个ABI中,按值函数都可以在寄存器中传递其自变量。但是,这有其局限性。这是 fT1T2twop的副引用版本-包含两个指针的结构,Linux gcc:
auto f_ref<twop, twop>(std::pair<twop, twop> const&):
mov rax, rdi
mov r8, QWORD PTR [rsi]
mov rdi, QWORD PTR [rsi+8]
mov rcx, QWORD PTR [rsi+16]
mov rdx, QWORD PTR [rsi+24]
mov QWORD PTR [rax], r8
mov QWORD PTR [rax+8], rdi
mov QWORD PTR [rax+16], rcx
mov QWORD PTR [rax+24], rdx

这是按值(value)的版本:
auto f_val<twop, twop>(std::pair<twop, twop>):
mov rdx, QWORD PTR [rsp+8]
mov rax, rdi
mov QWORD PTR [rdi], rdx
mov rdx, QWORD PTR [rsp+16]
mov QWORD PTR [rdi+8], rdx
mov rdx, QWORD PTR [rsp+24]
mov QWORD PTR [rdi+16], rdx
mov rdx, QWORD PTR [rsp+32]
mov QWORD PTR [rdi+24], rdx

尽管加载和存储的顺序不同,但是两者的作用完全相同:4个加载和4个存储,从输入到输出复制32个字节。唯一的实际区别是,在按值的情况下,对象应该在堆栈上(因此,我们从 [rsp]复制),在按引用的情况下,对象由第一个参数指向,因此我们从 [rdi] 1复制。

因此,存在一个较小的窗口,其中非内联值函数比传递引用具有优势:可以在寄存器中传递其参数的窗口。对于Sys V ABI,这通常适用于最大16个字节的结构,而在Windows x86-64 ABI上,最大8个字节。还有其他限制,因此并非所有此大小的对象总是在寄存器中传递。

1您可能会说,嘿, rdi接受第一个参数,而不是 rsi-但是这里发生的是返回值也必须通过内存传递,因此有一个隐藏的第一个参数-指向返回值的目标缓冲区的指针-被隐式使用,并进入 rdi

关于c++ - 作为 `const&`轻量级对象传递,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57349068/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com