[英]Passing as `const&` lightweight objects
給出以下寵物片段:
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
的構造函數,如果讓它們接收副本比引用更好的話。
調用上下文是未知的,但是對象生成器和類構造函數本身都是內聯函數,因此,引用和值之間的差異並不重要,因為優化器可以看到最終目標並在路的盡頭應用構造(I'我只是推測),因此對象生成器將是純零開銷的抽象,在這種情況下,我認為如果某些例外的配對對比平常大,則引用會更好。
但是,如果不是這種情況(即使對所有內容都是內聯的,引用也總是或通常對副本有一定影響),那么我將尋求副本。
在微優化領域之外,我通常會傳遞const
引用,因為您沒有修改對象,並且希望避免復制。 如果有一天您確實使用了構造成本很高的T1
或T2
,則副本可能會成為一個大問題:傳遞const引用並沒有同樣強大的步槍。 因此,我將按值傳遞作為具有非常不對稱權衡的選擇,並且僅當我知道數據很小時才按值選擇。
至於您的特定微優化問題,它基本上取決於調用是否完全內聯以及您的編譯器是否正常。
如果f
函數的任一變體內聯到調用程序中,並且啟用了優化,則對於任何一個變體,您可能都將獲得相同或幾乎相同的代碼。 我在這里使用inline_f_ref
和inline_r_val
調用進行測試。 它們都從一個未知的外部函數生成一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
痕跡都mypair
了。
這是一個版本,其中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比平均值對內聯函數更不情願(例如,在沒有inline
關鍵字的情況下,它沒有在-O2
處內聯非常短的函數)。
如果沒有內聯參數傳遞和返回的方式,則由ABI設置,因此編譯器無法優化兩個版本之間的差異。 const
參考版本相當於傳遞一個指針,因此無論T1
和T2
您都將傳遞一個指針到第一個整數寄存器中的std::pair
對象。
這是在Linux上的gcc中T1
和T2
為int
時的代碼:
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無法在rax
返回mypair<int,int>
。 而是,調用方在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位大小的參數傳遞到寄存器中,因此負載消失了。
然后編譯器開始做一個非常愚蠢的事情:從rax
的std::pair
的64位開始,它寫出低32位,將高32位移到底部,然后將其寫出。 世界上最慢的僅寫出64位的方式。 不過,此代碼通常比引用版本要快。
在兩個ABI中,按值函數都可以在寄存器中傳遞其自變量。 但是,這有其局限性。 這是T1
和T2
為twop
時f
的按引用版本-一種包含兩個指針的結構,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
這是按價值的版本:
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
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.