给出以下宠物片段:
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); }
如果我知道T1
和T2
都是轻量级对象,它们的复制时间可以忽略(例如,每个指针有两个),那么最好将std::pair
作为副本传递比作为参考?因为我知道有时候让编译器省略副本比强迫它处理引用(例如,优化复制链)更好。
如果让my_pair
的构造函数接收副本比引用的副本更好,那么该问题同样适用于UITableview
。
调用上下文是未知的,但是对象生成器和类构造函数本身都是内联函数,因此引用和值之间的差异可能并不重要,因为优化器可以看到最终目标并在道路尽头应用构造(我只是在推测),因此对象生成器将是纯零开销的抽象,在这种情况下,我认为如果某些异常对比平时更大,则引用会更好。
但是,如果不是这种情况(即使对所有内容都是内联引用,引用总是或通常会对副本产生一些影响),然后我会去寻找副本。
答案 0 :(得分:3)
在微优化领域之外,我通常会传递一个const
引用,因为您没有修改对象,并且希望避免复制。如果有一天您要使用昂贵的T1
或T2
进行复制,则复制可能是一个大问题:传递const引用并没有同样强大的步枪。因此,我将按值传递作为具有非常不对称权衡的选择,并且仅在知道数据很小时才按值选择。
关于您的特定的微优化问题,它基本上取决于调用是否完全内联以及您的编译器是否正确。
如果您的f
函数的任一变体被内联到调用者中,并且启用了优化,则对于任何一个变体,您可能都将获得相同或几乎相同的代码。我通过inline_f_ref
和inline_r_val
调用测试了here。它们都从未知的外部函数生成pair
,然后调用f
的按引用或变量。
像f_val
一样(f_ref
版本仅在结尾更改呼叫):
template <typename T>
auto inline_f_val() {
auto pair = get_pair<T>();
return f_val(pair);
}
当T1
和T2
为int
时,这是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::pair
和mypair
实际上具有相同的布局,因此所有f
的痕迹都消失了。
这里是一个T1
和T2
是带有两个指针的结构的版本,相反:
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
关键字的情况下,它没有内联非常短函数)。 / p>
没有内联参数传递和返回的方式是由ABI设置的,因此编译器无法优化消除两个版本之间的差异。 inline
参考版本相当于传递一个指针,因此无论const
和T1
,您都将传递一个指针到第一个整数寄存器中的T2
对象。>
这是在Linux上的gcc中std::pair
和T1
为T2
时的代码:
int
auto f_ref<int, int>(std::pair<int, int> const&):
mov rax, QWORD PTR [rdi]
ret
的指针在std::pair
中传递,因此该函数的主体是从该位置到rdi
的单个8字节移动。 rax
占用8个字节,因此编译器一次完成复制整个过程。在这种情况下,返回值在std::pair<int, int>
中“按值”传递,因此就可以完成了。
这取决于编译器的优化能力和ABI。例如,这是MSVC为64位Windows目标编译的同一函数:
rax
这里发生了两种不同的事情。首先,ABI是不同的。 MSVC无法在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
中返回mypair<int,int>
。相反,呼叫者将{em> pointer 传递给rax
到被呼叫者应保存结果的位置。因此,此功能除负载外还具有存储功能。 rcx
加载了已保存数据的位置。第二件事是,编译器太笨拙,无法将两个相邻的4字节加载组合在一起并存储为8字节加载,因此有两个加载和两个存储。
第二部分可以由更好的编译器修复,但是第一部分是API的结果。
这是此功能的按值版本,在Linux上的gcc中:
rax
仅保留一条指令,但这次仅执行一次reg-reg移动,这永远不会比加载程序贵,并且通常便宜得多。
在MSVC(64位Windows)上:
auto f_val<int, int>(std::pair<int, int>):
mov rax, rdi
ret
您仍然有两个存储区,因为ABI仍会强制将值返回到内存中,但是负载消失了,因为MSVC 64位API允许参数最大64位大小在寄存器中传递。
然后编译器开始做一个非常愚蠢的事情:从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
中的std::pair
的64位开始,它写出最低的32位,将最高的32位移至最低,然后把那些写出来。世界上最慢的仅写出64位的方式。仍然,此代码通常比按引用版本要快。
在两个ABI中,按值函数都可以在寄存器中传递其自变量。但是,这有其局限性。这是rax
和f
为T1
时T2
的引用版本-一种包含两个指针的结构,Linux gcc:
twop
以下是按值显示的版本:
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
尽管加载和存储的顺序不同,但两者的作用完全相同:4个加载和4个存储,从输入到输出复制32个字节。唯一真正的区别是,在按值排序的情况下,对象应该在堆栈上(因此我们从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
复制),而在按引用方式的情况下,对象由第一个参数指向,因此我们复制来自[rsp]
] 1 。
因此,有一个很小的窗口,其中非内联值函数比按引用传递有一个优势:可以在寄存器中传递其参数的窗口。对于Sys V ABI,这通常适用于最大16个字节的结构,而在Windows x86-64 ABI上,最大8个字节。还有其他限制,因此并非所有此大小的对象总是在寄存器中传递。
1 您可能会说,嘿,[rdi
接受第一个参数,而不是rdi
-但是这里发生的是还必须传递返回值内存,因此隐式使用了一个隐藏的第一个参数(指向返回值的目标缓冲区的指针),该参数进入rsi
。