Visual Studio:優(yōu)化了復(fù)制/移動(dòng)省略
蝎子
為了能發(fā)文,標(biāo)題中的復(fù)制/移動(dòng)省略是 Copy/Move Elision 的硬翻譯,請(qǐng)各位大大海涵。下文中我會(huì)同時(shí)使用這兩種術(shù)語(yǔ)。
Visual Studio 中 Copy/Move Elision 的變化
在 Visual Studio 2022 版本 17.4 預(yù)覽版 3 中,我們顯著增加了適用于Copy/Move Elision 情況的數(shù)量,并讓用戶(hù)能夠更好地控制是否啟用這些轉(zhuǎn)換。
Copy/Move Elision 是什么?
當(dāng) C++ 函數(shù)中的 return 關(guān)鍵字后跟非內(nèi)置類(lèi)型的表達(dá)式時(shí),執(zhí)行該 return 語(yǔ)句會(huì)將表達(dá)式的結(jié)果復(fù)制到調(diào)用函數(shù)的返回槽(Return Slot)中。為此,將調(diào)用非內(nèi)置類(lèi)型的復(fù)制或移動(dòng)構(gòu)造函數(shù)。然后,作為退出函數(shù)的一部分,將調(diào)用函數(shù)局部變量的析構(gòu)函數(shù),可能包括 return 關(guān)鍵字后面的表達(dá)式中命名的任何變量。
C++ 規(guī)范允許編譯器直接在調(diào)用函數(shù)的返回槽中構(gòu)造返回的對(duì)象,從而省略作為返回的一部分執(zhí)行的復(fù)制或移動(dòng)構(gòu)造函數(shù)。與大多數(shù)其他優(yōu)化不同,這種轉(zhuǎn)換允許對(duì)程序的輸出產(chǎn)生可觀察的影響 – 即復(fù)制或移動(dòng)構(gòu)造函數(shù)以及關(guān)聯(lián)的析構(gòu)函數(shù)可以少調(diào)用一次。
Visual Studio 中的 Copy/Move Elision
C++ 標(biāo)準(zhǔn)要求在將返回值初始化為 return 語(yǔ)句的一部分時(shí)(例如,當(dāng)返回類(lèi)型為 Foo 的函數(shù)返回返回 Foo()時(shí)),編譯器需要執(zhí)行 Copy/Move Elision。Microsoft Visual C++ 編譯器始終根據(jù)需要對(duì)返回語(yǔ)句執(zhí)行 Copy/Move Elision,而不管傳遞給編譯器的標(biāo)志如何。此行為保持不變。
在 Visual Studio 17.4 預(yù)覽版 3 中對(duì)可選 Copy/Move Elision 的更改
當(dāng)返回的值為命名變量時(shí),編譯器可能會(huì)省略復(fù)制或移動(dòng),但不是必需的。C++ 標(biāo)準(zhǔn)仍要求為命名的返回變量定義復(fù)制或移動(dòng)構(gòu)造函數(shù),即使編譯器在所有情況下都省略了構(gòu)造函數(shù)。在 Visual Studio 2022 版本 17.4 預(yù)覽版 3 之前,當(dāng)禁用優(yōu)化(例如使用 /Od 編譯器標(biāo)志或使用了 #pragma optimize(“”,off))時(shí),編譯器將僅執(zhí)行強(qiáng)制Copy/Move Elision。使用 /O2 標(biāo)志,編譯器將通過(guò)簡(jiǎn)單的控制流為優(yōu)化的函數(shù)執(zhí)行可選的Copy/Move Elision。
從 Visual Studio 2022 版本 17.4 預(yù)覽版 3 開(kāi)始,我們?yōu)殚_(kāi)發(fā)人員提供了與新的 /Zc:nrvo 編譯器標(biāo)志保持一致的選項(xiàng)。默認(rèn)情況下,當(dāng)使用 /O2 標(biāo)志、/permissive- 編譯代碼時(shí),或者在為 /std:c++20 或更高版本進(jìn)行編譯時(shí),將傳遞 /Zc:nrvo 標(biāo)志。通過(guò)此標(biāo)志后,將盡可能執(zhí)行復(fù)制和移動(dòng)省略。我們希望在將來(lái)的版本中默認(rèn)啟用 /Zc:nrvo。另外,開(kāi)發(fā)者還可以使用 /Zc:nrvo- 標(biāo)志顯式禁用可選的Copy/Move Elision。請(qǐng)注意,無(wú)法禁用強(qiáng)制型的Copy/Move Elision。
在 Visual Studio 2022 版本 17.4 預(yù)覽版 3 中,當(dāng)使用 /Zc:nrvo、/O2、/permissive-或 /std:c++20 或更高版本的標(biāo)志啟用可選復(fù)制/移動(dòng)省略時(shí),我們還增加了Copy/Move Elision的位置。
可選 Copy/Move Elision 的示例
可選 Copy/Move Elision 的最簡(jiǎn)單示例是以下函數(shù):Foo SimpleReturn() {Foo result;return result;}
在這種情況下,如果傳遞了 /O2 標(biāo)志,則早期版本的 MSVC 編譯器已將結(jié)果的復(fù)制或移動(dòng)到返回槽中。在 Visual Studio 2022 版本 17.4 預(yù)覽版 3 中,如果傳遞了 /permissive-、/std:c++20 或更高版本或 /Zc:nrvo 標(biāo)志,也會(huì)省略復(fù)制或移動(dòng),如果傳遞了 /Zc:nrvo- 標(biāo)志,則保留復(fù)制或移動(dòng)。
從 Visual Studio 2022 版本 17.4 預(yù)覽版 3 開(kāi)始,如果將 /O2、/permissive-、/std:c++20 或更高版本或 /Zc:nrvo 標(biāo)志傳遞給編譯器,而 /Zc:nrvo- 標(biāo)志未傳遞到編譯器,我們現(xiàn)在在以下其他情況下執(zhí)行復(fù)制/移動(dòng)省略。
在循環(huán)中返回
Foo ReturnInALoop(int iterations) {for (int i = 0; i < iterations; ++i) {Foo result;if (i == (iterations / 2)) {return result;}}}結(jié)果對(duì)象將在循環(huán)的每次迭代開(kāi)始時(shí)正確構(gòu)造,并在每次迭代結(jié)束時(shí)銷(xiāo)毀。在返回結(jié)果的迭代中,退出函數(shù)時(shí)不會(huì)調(diào)用其析構(gòu)函數(shù)。當(dāng)返回的對(duì)象超出該函數(shù)的范圍時(shí),函數(shù)的調(diào)用方將銷(xiāo)毀該對(duì)象。
在異常處理中返回
如果傳遞了 /O2、/permissive-、/std:c++20 或更高版本,或者傳遞了 /Zc:nrvo 標(biāo)志,而 /Zc:nrvo- 標(biāo)志未傳遞,則結(jié)果對(duì)象的復(fù)制或移動(dòng)現(xiàn)在將被省略。我們現(xiàn)在還可以妥善處理更復(fù)雜的情況,例如:
結(jié)果對(duì)象將在調(diào)用方函數(shù)的返回槽中構(gòu)造,并且在成功返回時(shí)不會(huì)為其調(diào)用復(fù)制/移動(dòng)構(gòu)造函數(shù)或析構(gòu)函數(shù)。引發(fā)異常時(shí),是否析構(gòu)結(jié)果對(duì)象取決于向編譯器傳遞哪些異常處理標(biāo)志。默認(rèn)情況下,不會(huì)發(fā)生堆棧展開(kāi),因此不會(huì)調(diào)用析構(gòu)函數(shù)。但是,如果使用 /EHs、/EHa 或 /EHr 標(biāo)志啟用了堆棧展開(kāi)異常處理,則 goto Label1 將導(dǎo)致調(diào)用結(jié)果的析構(gòu)函數(shù),因?yàn)樗D(zhuǎn)到初始化結(jié)果之前。無(wú)論哪種方式,當(dāng)再次到達(dá)表達(dá)式 Foo 結(jié)果時(shí),將在返回槽中再次構(gòu)造對(duì)象。
復(fù)制具有默認(rèn)參數(shù)的構(gòu)造函數(shù)
現(xiàn)在,我們可以正確檢測(cè)到具有默認(rèn)參數(shù)的復(fù)制或移動(dòng)構(gòu)造函數(shù)仍然是復(fù)制或移動(dòng)構(gòu)造函數(shù),因此可以在上述情況下被省略。具有默認(rèn)參數(shù)的復(fù)制構(gòu)造函數(shù)如下所示:structStructWithCopyConstructorDefaultParam {int X;
對(duì)NRVO的限制
盡管 MSVC 編譯器現(xiàn)在在更多情況下執(zhí)行Copy/Move Elision,但并不總是能夠執(zhí)行它。若要了解為什么會(huì)這樣,請(qǐng)考慮以下函數(shù):
復(fù)制省略構(gòu)造要在返回槽中返回的對(duì)象,但在這種情況下,應(yīng)在返回槽中構(gòu)造哪個(gè)對(duì)象?為了在返回結(jié)果A時(shí)省略結(jié)果A的副本,必須在返回槽中構(gòu)造它。但是,如果條件為真,則需要在銷(xiāo)毀結(jié)果 A 之前在返回槽中構(gòu)造結(jié)果 B。無(wú)法對(duì)兩個(gè)路徑執(zhí)行復(fù)制省略。
我們目前選擇避免在函數(shù)中的所有路徑上執(zhí)行可選的Copy/Move Elision,如果在任何路徑上它是不可能的的話。但是,對(duì)內(nèi)聯(lián)決策、死代碼消除和其他優(yōu)化的更改可能會(huì)更改Copy/Move Elision的可能性。因此,編寫(xiě)依賴(lài)于命名變量的Copy/Move Elision的某些行為的代碼是不安全的,除非使用 /Zc:nrvo- 禁用了所有可選的Copy/Move Elision。
只要啟用了堆棧展開(kāi)異常處理或未引發(fā)異常,仍然可以安全地假定每個(gè)構(gòu)造函數(shù)調(diào)用都有匹配的析構(gòu)函數(shù)調(diào)用。
總結(jié)
寫(xiě)著舊時(shí)代的 C++,一直都為如何高性能地返回一個(gè)對(duì)象發(fā)愁。沒(méi)錯(cuò),正是在下。