高級(jí)編程語(yǔ)言提供的函數(shù)、條件語(yǔ)句和循環(huán)這樣的抽象編程構(gòu)造極大地提高了編程效率。然而,這也潛在地使性能顯著下降成為了用高級(jí)編程語(yǔ)言寫(xiě)程序的一大劣勢(shì)。在理想條件下,在不以性能為妥協(xié)的情況下,你應(yīng)該寫(xiě)出易讀并且易維護(hù)的代碼。因此,編譯器嘗試自動(dòng)優(yōu)化代碼以提高其性能,當(dāng)今的編譯器都深諳其道。編譯器可以轉(zhuǎn)化循環(huán)、條件語(yǔ)句和遞歸函數(shù)、消除整塊代碼和利用目標(biāo)指令集的優(yōu)勢(shì)讓代碼變得高效而簡(jiǎn)潔。所以對(duì)程序員來(lái)說(shuō),寫(xiě)出可讀性高的代碼要比因?yàn)槭止?yōu)化而使代碼變得神秘且難以維護(hù)更加可貴。事實(shí)上,手工優(yōu)化的代碼反而可能會(huì)讓編譯器難以進(jìn)行額外和更加有效的優(yōu)化。
比起手工優(yōu)化代碼,你更應(yīng)該考慮關(guān)于設(shè)計(jì)的各個(gè)方面,比如使用更快的算法,引入線(xiàn)程級(jí)并行機(jī)制和利用框架特性(比如move構(gòu)造函數(shù))。
這篇文章是關(guān)于Visual C++ 編譯器優(yōu)化的。為了便于應(yīng)用,我將會(huì)討論編譯器采取的最重要的優(yōu)化技巧和決策。我的目的不是告訴你如何手工優(yōu)化代碼,而是向你展示為什么你可以信賴(lài)編譯器來(lái)優(yōu)化你寫(xiě)出的代碼。這篇文件絕不是對(duì)Visual C++ 編譯器優(yōu)化工作的全面考察。但是將會(huì)給你展示那些你真正想要了解的優(yōu)化工作和怎樣與你的編譯器溝通來(lái)應(yīng)用它們。
有一些重要的優(yōu)化是超出所有現(xiàn)有編譯器能力的——比如,用高效的算法代替低效的,或者改變數(shù)據(jù)結(jié)構(gòu)的排列以?xún)?yōu)化其在內(nèi)存中的布局。但是這些優(yōu)化話(huà)題超出了本文的范圍。
定義編譯器優(yōu)化
優(yōu)化工作涉及到的一個(gè)方面,是把一行代碼轉(zhuǎn)化成同等效果的另一行代碼,在這個(gè)過(guò)程中提升它的一項(xiàng)或多項(xiàng)性能。最重要的兩項(xiàng)性能(指標(biāo))是代碼的執(zhí)行速度和長(zhǎng)度。其他一些特性包括代碼執(zhí)行開(kāi)銷(xiāo),代碼編譯所需時(shí)間,如果代碼需要通過(guò)即時(shí)編譯機(jī)制(Just-in-Time (JIT))進(jìn)行編譯,那么JIT所需的編譯時(shí)間也是指標(biāo)之一。
編譯器經(jīng)常會(huì)依據(jù)它們所使用的技術(shù)優(yōu)化代碼。雖然并不完美,但是比起花時(shí)間手工苦苦推敲一個(gè)程序,利用編譯器提供的特有功能和讓編譯器來(lái)優(yōu)化代碼要高效得多。
這里有4種方法讓你的編譯器更加高效地優(yōu)化代碼:
書(shū)寫(xiě)可讀、高效的代碼。不要把Visual C++ 面向?qū)ο蟮奶匦援?dāng)作性能的敵人。最新版本的C++可以讓這些開(kāi)銷(xiāo)保持到最低甚至消除這些開(kāi)銷(xiāo)。
2.使用編譯器聲明。例如讓編譯器使用比默認(rèn)情況更快的函數(shù)調(diào)用約定。
3.使用編譯器內(nèi)置函數(shù)(compiler-intrinsic functions)。內(nèi)在函數(shù)是其實(shí)現(xiàn)由編譯器自動(dòng)提供的特殊函數(shù)。編譯器對(duì)其很熟悉并且會(huì)用極其高效的指令序列來(lái)代替函數(shù)調(diào)用,以充分利用目標(biāo)指令集的優(yōu)勢(shì)。當(dāng)前Microsoft .NET Framework不支持編譯器內(nèi)置函數(shù),因此其下的語(yǔ)言都不支持。但是Visual C++ 對(duì)這一特性有外在支持。注意,雖然使用內(nèi)置函數(shù)能夠提升代碼性能,但是會(huì)降低可讀性和可移植性。
4. 使用性能分析引導(dǎo)優(yōu)化(profile-guided optimization)。使用這一技術(shù),可以讓編譯器搜集更多關(guān)于代碼的運(yùn)行時(shí)行為,并且以此來(lái)作為優(yōu)化依據(jù)。
本文的目的是通過(guò)證明編譯器可以在低效但是可讀性強(qiáng)的代碼上應(yīng)用優(yōu)化(應(yīng)用第一條方法),從而向你展示為什么你可以信任編譯器。當(dāng)然我也會(huì)提供一些對(duì)性能分析引導(dǎo)優(yōu)化(profile-guided optimization)的簡(jiǎn)短說(shuō)明,和提到一些可以微調(diào)代碼的編譯器聲明。
編譯器有許多優(yōu)化技巧,從像常量折疊這樣簡(jiǎn)單的變換,直到像指令重排(instruction scheduling)這樣極其復(fù)雜的變換。然而在這篇文章中我只有限地討論了一些最重要的優(yōu)化——那些可以顯著地提升性能(兩位數(shù)的百分?jǐn)?shù)來(lái)衡量)和減少代碼長(zhǎng)度的優(yōu)化:內(nèi)聯(lián)函數(shù)(function inlining)、COMDAT優(yōu)化(COMDAT optimizations)和循環(huán)優(yōu)化。我將會(huì)在下一部分討論前兩個(gè)話(huà)題,然后展示你如何控制Visual C++實(shí)現(xiàn)優(yōu)化。最后會(huì)有.NET Framework優(yōu)化的簡(jiǎn)略說(shuō)明。通篇我都將會(huì)采用Visual Studio 2013來(lái)構(gòu)建代碼。
鏈接時(shí)代碼生成
鏈接時(shí)代碼生成(LTCG)是一項(xiàng)應(yīng)用在C/C++代碼上的程序全局優(yōu)化(WPO)技術(shù)。C/C++編譯器獨(dú)立地編譯每個(gè)源文件然后產(chǎn)生出相應(yīng)的目標(biāo)文件。這意味著編譯器只能在單個(gè)源文件上應(yīng)用優(yōu)化技術(shù),而無(wú)法照顧到整個(gè)程序。但是,一些重要的優(yōu)化卻只能瀏覽全部程序后才能產(chǎn)生。所以你只能在鏈接時(shí)(link time)應(yīng)用這些優(yōu)化,而非編譯時(shí)(compile time),因?yàn)殒溄悠骺梢酝暾乜吹匠绦颉?/P>
當(dāng)LTGC被打開(kāi)時(shí)(通過(guò)指定編譯器開(kāi)關(guān)/GL),編譯器驅(qū)動(dòng)程序(cl.exe)將只調(diào)用編譯器前端(c1.dll or c1xx.dll),并把后端調(diào)用(c2.dll)推遲到鏈接時(shí)間。產(chǎn)出的目標(biāo)文件包含通用中間語(yǔ)言(Common Intermediate Language——CIL)代碼,而不是依賴(lài)機(jī)器的匯編代碼。然后,當(dāng)鏈接器(link.exe)被調(diào)用,它就能看到包含C中間語(yǔ)言的目標(biāo)文件,并調(diào)用編譯器后端,依次進(jìn)行程序全局優(yōu)化,生成二進(jìn)制目標(biāo)文件,再返回鏈接器把所有目標(biāo)文件鏈接在一起,最后生成可執(zhí)行文件。
編譯器前端實(shí)際上進(jìn)行了一些優(yōu)化,比如無(wú)論優(yōu)化啟用還是禁用,都會(huì)進(jìn)行常量折疊。但是所有重要的優(yōu)化工作都是在編譯器后端進(jìn)行的,并且可以使用編譯器開(kāi)關(guān)控制。
鏈接時(shí)代碼生成(LTCG)能讓后端積極地執(zhí)行許多優(yōu)化(通過(guò)指定/GL與/O1或/O2,以及/Gw編譯器開(kāi)關(guān),和/OPT:REF 與 /OPT:ICF鏈接器開(kāi)關(guān))。在本文中,討論僅限于內(nèi)聯(lián)函數(shù)(function inlining)和COMDAT優(yōu)化(COMDAT optimizations)。關(guān)于完整的鏈接時(shí)代碼生成優(yōu)化,請(qǐng)參考相關(guān)文檔。注意鏈接器可以在本地目標(biāo)文件,本地/托管混合目標(biāo)文件,純托管目標(biāo)文件,安全托管目標(biāo)文件和安全.net模塊上執(zhí)行鏈接時(shí)代碼生成。
我編寫(xiě)了一個(gè)包含兩個(gè)源文件(source1.c 和 source2.c)和一個(gè)頭文件(source2.h)的程序。source1.c 和 source2.c分別在Figure 1 and Figure 2中。由于頭文件中非常簡(jiǎn)單地包含了source2.c中的函數(shù)原型, 所以并沒(méi)有列出。
Figure 1 The source1.c File
#include <stdio.h> // scanf_s and printf.
#include "Source2.h"
int square(int x) { return x*x; }
main() {
int n = 5, m;
scanf_s("%d", &m);
printf("The square of %d is %d.", n, square(n));
printf("The square of %d is %d.", m, square(m));
printf("The cube of %d is %d.", n, cube(n));
printf("The sum of %d is %d.", n, sum(n));
printf("The sum of cubes of %d is %d.", n, sumOfCubes(n));
printf("The %dth prime number is %d.", n, getPrime(n));
}
Figure 2 The source2.c File
#include <math.h> // sqrt.
#include <stdbool.h> // bool, true and false.
#include "Source2.h"
int cube(int x) { return x*x*x; }
int sum(int x) {
int result = 0;
for (int i = 1; i <= x; ++i) result += i;
return result;
}
int sumOfCubes(int x) {
int result = 0;
for (int i = 1; i <= x; ++i) result += cube(i);
return result;
}
static
bool isPrime(int x) {
for (int i = 2; i <= (int)sqrt(x); ++i) {
if (x % i == 0) return false;
}
return true;
}
int getPrime(int x) {
int count = 0;
int candidate = 2;
while (count != x) {
if (isPrime(candidate))
++count;
}
return candidate;
}
source1.c文件包含兩個(gè)函數(shù),有一個(gè)參數(shù)并返回這個(gè)參數(shù)的平方的square函數(shù),以及程序的main函數(shù)。main函數(shù)調(diào)用source2.c中除了isPrime之外的所有函數(shù)。source2.c有5個(gè)函數(shù)。cube返回一個(gè)數(shù)的三次方;sum函數(shù)返回從1到給定數(shù)的和;sumOfcubes返回1到給定數(shù)的三次方的和;isPrime用于判斷一個(gè)數(shù)是否是質(zhì)數(shù);getPrime函數(shù)返回第x個(gè)質(zhì)數(shù)。我省略掉了容錯(cuò)處理因?yàn)槟遣⒎潜疚牡闹攸c(diǎn)。
這些代碼簡(jiǎn)單但是很有用。其中一些函數(shù)只進(jìn)行簡(jiǎn)單的運(yùn)算,一些需要簡(jiǎn)單的循環(huán)。getPrime是當(dāng)中最復(fù)雜的函數(shù),包含一個(gè)while循環(huán)且在循環(huán)內(nèi)部調(diào)用了也包含一個(gè)循環(huán)的isPrime函數(shù)。我將會(huì)利用這些函數(shù)證實(shí)被稱(chēng)作內(nèi)聯(lián)函數(shù)的優(yōu)化,和一些其他的優(yōu)化,其中內(nèi)聯(lián)函數(shù)這是編譯器最重要的優(yōu)化之一。
我會(huì)在三種不同的配置下生成代碼并且檢驗(yàn)結(jié)果來(lái)驗(yàn)證代碼是如何被編譯器轉(zhuǎn)化的。如果你也照做的話(huà),你需要匯編生成文件(由編譯器開(kāi)關(guān)/FA[s]生成)來(lái)檢驗(yàn)生成的匯編代碼以及映像文件(由鏈接器開(kāi)關(guān)/MAP生成)來(lái)檢驗(yàn)初始化數(shù)據(jù)優(yōu)化是否被執(zhí)行(如果你指定了/verbose:icf 和 /verbose:ref開(kāi)關(guān),鏈接器也可以匯報(bào)這一項(xiàng))。因此你需要確保在接下來(lái)的配置中指定了上述開(kāi)關(guān)。我也會(huì)使用C編譯器(/TC)以讓生成的代碼容易檢驗(yàn)。但是這篇文章中所有我討論的東西對(duì)于C++一樣適用。
Debug配置
之所以使用Debug配置,是因?yàn)樵谀愦蜷_(kāi)了編譯器/Od開(kāi)關(guān)而沒(méi)有打開(kāi)/GL開(kāi)關(guān)時(shí),所有的后端優(yōu)化都是禁用的。當(dāng)在這項(xiàng)配置下構(gòu)建代碼時(shí),生成的目標(biāo)文件將包含和源代碼完全對(duì)應(yīng)的二進(jìn)制代碼。你可以通過(guò)生成的匯編輸出文件和映像文件來(lái)確認(rèn)這一點(diǎn)。這項(xiàng)配置相當(dāng)于Visual Studio中的調(diào)試配置。
編譯時(shí)代碼生成Release配置
這項(xiàng)配置和優(yōu)化被啟用的配置(通過(guò)指定/O1,/O2或/Ox編譯器開(kāi)關(guān))非常相似,但是不指定/GL編譯器開(kāi)關(guān)。在這項(xiàng)配置下,生成目標(biāo)文件將包含優(yōu)化過(guò)的二進(jìn)制代碼。但是沒(méi)有整個(gè)程序級(jí)別的優(yōu)化。
通過(guò)查看source1.c生成的匯編代碼文件,你會(huì)看到執(zhí)行了兩項(xiàng)優(yōu)化。首先,通過(guò)在編譯時(shí)的評(píng)估計(jì)算把square函數(shù)的第一次調(diào)用完全刪去了。這是如何發(fā)生的呢?編譯器發(fā)現(xiàn)square函數(shù)很小,它應(yīng)該被作為內(nèi)聯(lián)函數(shù)。將它作為內(nèi)聯(lián)函數(shù)之后,編譯器發(fā)現(xiàn)本地變量n的值是已知的并且在給它賦值和調(diào)用函數(shù)之間沒(méi)有發(fā)生改變。因此,編譯器總結(jié)出執(zhí)行乘法和用25替代結(jié)果是安全的。第二項(xiàng)優(yōu)化,對(duì)于square的第二次調(diào)用square(m),也被當(dāng)作內(nèi)聯(lián)函數(shù)。但是,因?yàn)閙的值在編譯時(shí)是未知的,所以編譯器不能對(duì)計(jì)算估值,所以事實(shí)上代碼被保留了。
現(xiàn)在我會(huì)檢查source2.c的匯編代碼文件,這將會(huì)更有趣。在函數(shù)sumOfCubes內(nèi)對(duì)cube的調(diào)用被作為內(nèi)聯(lián)函數(shù)。這會(huì)讓編譯器啟用了對(duì)循環(huán)來(lái)說(shuō)意義重大的一些優(yōu)化(如你在“循環(huán)優(yōu)化”部分將看到的)。此外,SSE2指令集被用于在isPrime函數(shù)中,當(dāng)調(diào)用了sqrt函數(shù)時(shí)把int轉(zhuǎn)化為double而在sqrt返回值時(shí)又把double轉(zhuǎn)化為int。并且sqrt只在循環(huán)開(kāi)始前調(diào)用了一次。注意如果/arch編譯器開(kāi)關(guān)沒(méi)有被打開(kāi),x86編譯器將會(huì)默認(rèn)使用SSE2。大多數(shù)x86處理器以及所有x86-64處理器,都支持SSE2。
鏈接時(shí)代碼生成Release配置
鏈接時(shí)代碼生成(LTCG) Relase配置與Visual Studio中的Release配置相同。在這項(xiàng)配置中,優(yōu)化被啟用并且/GL編譯器開(kāi)關(guān)被打開(kāi)。這個(gè)開(kāi)關(guān)隱含的指定了使用/O1或者/O2。這告訴編譯器生成通用中間語(yǔ)言(Common Intermediate Language——CIL)目標(biāo)文件而不是匯編目標(biāo)文件。這樣,鏈接器像之前所說(shuō)那樣調(diào)用編譯器的后端來(lái)執(zhí)行整個(gè)程序的優(yōu)化。現(xiàn)在我將會(huì)討論一些程序全局優(yōu)化來(lái)展示鏈接時(shí)代碼生成帶來(lái)的巨大好處。這項(xiàng)配置所生成的匯編代碼列表可以在網(wǎng)絡(luò)上得到。
只要允許函數(shù)被內(nèi)聯(lián)(/Ob控制,不論何時(shí),只要需要優(yōu)化就可以打開(kāi)),不論/Gy開(kāi)關(guān)(稍后討論)是否打開(kāi),/GL開(kāi)關(guān)都允許把其他翻譯單元中定義的函數(shù)作為內(nèi)聯(lián)函數(shù)。/LTCG鏈接器開(kāi)關(guān)是可選的并且只為鏈接器提供指導(dǎo)。
通過(guò)查看source1.c的匯編代碼,你會(huì)看到除了scanf_s之外的所有函數(shù)都被作為了內(nèi)聯(lián)函數(shù)。因此,編譯器被允許執(zhí)行函數(shù)cube,sum和sunOfCubes的計(jì)算。只有isPrime函數(shù)沒(méi)有被作為內(nèi)聯(lián)函數(shù)。但是,如果它被我們手動(dòng)在getPrime中寫(xiě)為內(nèi)聯(lián)函數(shù),編譯器仍然會(huì)在main函數(shù)中把getPrime作為內(nèi)聯(lián)函數(shù)。
正如你所見(jiàn),將函數(shù)內(nèi)聯(lián)很重要不僅僅是因?yàn)樗偸莾?yōu)化函數(shù)調(diào)用,而且它可以允許編譯器進(jìn)行許多其他優(yōu)化。將函數(shù)內(nèi)聯(lián)通常會(huì)以代碼量增加為代價(jià)來(lái)提升性能。過(guò)度地使用這一優(yōu)化會(huì)導(dǎo)致我們熟知的代碼膨脹現(xiàn)象。在每一次調(diào)用函數(shù)的地方,編譯器都會(huì)分析這樣做的利弊來(lái)決定是否將一個(gè)函數(shù)作為內(nèi)聯(lián)函數(shù)。
由于內(nèi)聯(lián)的重要性,Visual C++編譯器提供了比對(duì)內(nèi)聯(lián)的標(biāo)準(zhǔn)規(guī)定控制更多的支持。你可以通過(guò)使用auto_inline編譯控制編譯器不將一段范圍內(nèi)的函數(shù)內(nèi)聯(lián)。你可以通過(guò)標(biāo)記為_(kāi)_declspec(noinline)控制編譯器不把特定的函數(shù)或方法內(nèi)聯(lián)。你可以用關(guān)鍵字inline標(biāo)記一個(gè)函數(shù)來(lái)給編譯器提示將這個(gè)函數(shù)作為內(nèi)聯(lián)函數(shù)(雖然編譯器可能選擇忽略這一標(biāo)記如果這次內(nèi)聯(lián)帶來(lái)的是凈損失)。inline關(guān)鍵字從C++的第一個(gè)版本——C99,就可以使用了。你可以同時(shí)在C或者C++中使用微軟特有的關(guān)鍵字_inline,這在你使用不支持inline的老式C版本時(shí)是很有用的。并且,你可以使用__forceinline關(guān)鍵字(C和C++)來(lái)強(qiáng)制編譯器將任何可以?xún)?nèi)聯(lián)的函數(shù)內(nèi)聯(lián)。最后但是很重要的一點(diǎn)是,你可以告訴編譯器以確定或者不確定的深度拆開(kāi)一個(gè)遞歸函數(shù),這可以通過(guò)使用inline_recursion編譯指令來(lái)達(dá)成。注意編譯器當(dāng)下沒(méi)有提供任何特性可以讓你在函數(shù)調(diào)用時(shí)控制內(nèi)聯(lián),一切都只能在函數(shù)定義時(shí)控制。
默認(rèn)情況下生效的/Ob0開(kāi)關(guān)會(huì)完全禁用內(nèi)聯(lián)功能。你應(yīng)該在調(diào)試代碼時(shí)使用這一開(kāi)關(guān)(它在Visual Studio Debug配置下是自動(dòng)打開(kāi)的)。/Ob1開(kāi)關(guān)讓編譯器只在函數(shù)被定義為inline,__inline 或者_(dá)_forceinline時(shí),才考慮將函數(shù)內(nèi)聯(lián)。/Ob2開(kāi)關(guān)在指定了/O[1|2|x]時(shí)生效,編譯器將會(huì)考慮所有的函數(shù)是否可以?xún)?nèi)聯(lián)。在我看來(lái),只有在/Ob1控制內(nèi)聯(lián)時(shí)考慮是否使用inline或_inline才是有意義的。
在一些特定的條件下,編譯器是不能將函數(shù)內(nèi)聯(lián)的。舉個(gè)例子,當(dāng)虛調(diào)用一個(gè)虛函數(shù)時(shí),因?yàn)榫幾g器不知道哪個(gè)函數(shù)將會(huì)被調(diào)用,所以這個(gè)函數(shù)不能被內(nèi)聯(lián)。另一個(gè)例子是當(dāng)通過(guò)指針調(diào)用一個(gè)函數(shù)而不是通過(guò)函數(shù)名時(shí)。你應(yīng)該盡力避免這些條件來(lái)使得函數(shù)可以被內(nèi)聯(lián)。具體請(qǐng)參考MSDN文檔,那里列出了不能被內(nèi)聯(lián)的完整條件列表。
某些優(yōu)化,當(dāng)其作用于整個(gè)程序級(jí)別時(shí),往往比其作用于局部時(shí)更加有效,函數(shù)內(nèi)聯(lián)就是這種類(lèi)型的優(yōu)化之一。事實(shí)上,大多數(shù)優(yōu)化都在整體級(jí)別更加有效。在這一部分余下的內(nèi)容中,我將會(huì)討論被稱(chēng)作COMDAT優(yōu)化的一類(lèi)特定優(yōu)化。
默認(rèn)情況下,當(dāng)編譯翻譯單元時(shí),所有的代碼都被存儲(chǔ)到結(jié)果目標(biāo)文件的一個(gè)單獨(dú)區(qū)塊。鏈接器在單獨(dú)區(qū)塊的范疇上進(jìn)行操作:也就是對(duì)這些區(qū)塊進(jìn)行移除、合并或者重新排序。(但是)這種會(huì)妨礙鏈接器進(jìn)行三項(xiàng)優(yōu)化工作,而這三項(xiàng)優(yōu)化工作對(duì)顯著減少可執(zhí)行代碼量和提升性能又非常重要。第一項(xiàng)是消除未被引用的函數(shù)和全局變量;第二項(xiàng)是合并相同的函數(shù)和全局常量;第三項(xiàng)是重新對(duì)函數(shù)和全局變量排序,使得那些在同一路徑上執(zhí)行的函數(shù)和被一起訪問(wèn)的變量在物理內(nèi)存中離得更近,這會(huì)讓程序有更好的局部性。
為了能讓這些鏈接器優(yōu)化生效,你可以通過(guò)分別打開(kāi)/Gy(函數(shù)級(jí)別鏈接)和/Gw(全局?jǐn)?shù)據(jù)優(yōu)化)來(lái)分別讓編譯器對(duì)位于在不同區(qū)塊的函數(shù)和變量進(jìn)行打包操作。這些區(qū)塊被稱(chēng)為COMDATs。你也可以用__declspec( selectany)標(biāo)記特定的全局?jǐn)?shù)據(jù)變量來(lái)告訴編譯器把這個(gè)變量加入COMDAT。然后,通過(guò)指定/OPT:REF鏈接器開(kāi)關(guān),鏈接器就會(huì)刪去未被引用的函數(shù)和全局變量。你也可以通過(guò)指定/OPT:ICF開(kāi)關(guān),鏈接器就會(huì)合并相同的函數(shù)和全局常數(shù)變量。(ICF代表Identical COMDAT Folding。)通過(guò)/ORDER鏈接器開(kāi)關(guān),你可以讓鏈接器把COMDAT以特定的順序放入生成鏡像。注意所有的這些優(yōu)化都是鏈接器優(yōu)化所以不需要/GL開(kāi)關(guān)。如果是要對(duì)程序進(jìn)行調(diào)試,并且目的明確,那么/OPT:REF和/OPT:ICF開(kāi)關(guān)應(yīng)當(dāng)關(guān)閉。
你應(yīng)該盡可能使用鏈接時(shí)代碼生成(LTCG)。唯一不使用的原因是當(dāng)你想要分發(fā)生成的目標(biāo)文件和二進(jìn)制文件時(shí)。記得這些文件包含通用中間語(yǔ)言(CIL)而不是匯編語(yǔ)言,通用中間語(yǔ)言只能被生成它的特定版本的編譯器和鏈接器識(shí)別,這將會(huì)明顯限制目標(biāo)文件的使用,因?yàn)殚_(kāi)發(fā)者必須使用相同版本的編譯器以使用這些文件。這種情況下,除非你愿意為每個(gè)版本的編譯器都分發(fā)一份目標(biāo)文件,否則你應(yīng)該使用編譯時(shí)代碼生成。除了限制使用,這些目標(biāo)文件通常比相應(yīng)的匯編目標(biāo)文件更加龐大。但是記得CIL目標(biāo)文件帶來(lái)的巨大好處,那就是可以進(jìn)行程序全局優(yōu)化(WPO)。
循環(huán)優(yōu)化
Visual C++支持多種循環(huán)優(yōu)化,但是我只討論其中的3種:循環(huán)展開(kāi),自動(dòng)向量化和循環(huán)不變量代碼移動(dòng)。如果你修改了Figure1中的代碼讓m代替n作為sumOfCubes的參數(shù),編譯器將不能推斷出參數(shù)的值,所以必須讓函數(shù)可以處理任何參數(shù)。生成函數(shù)被高度優(yōu)化并且尺寸很大,所以編譯器不會(huì)將它作為內(nèi)聯(lián)函數(shù)。
用/O1生成匯編代碼,會(huì)在空間尺寸上進(jìn)行優(yōu)化。在這種情況下,不會(huì)對(duì)sumOfCubes函數(shù)實(shí)行任何優(yōu)化操作。用/O2生成代碼針對(duì)執(zhí)行速度進(jìn)行優(yōu)化。生成代碼的長(zhǎng)度會(huì)很長(zhǎng)但是執(zhí)行效率顯著提高,因?yàn)閟umOfCubes內(nèi)部的循環(huán)被展開(kāi)并且向量化了。有一個(gè)概念很重要,必須理解:如果不把cube函數(shù)內(nèi)聯(lián)就不能進(jìn)行向量化。而且,不進(jìn)行內(nèi)聯(lián)的話(huà)循環(huán)展開(kāi)并不會(huì)變得高效。Figure3 顯示了生成的匯編代碼的流程圖。這個(gè)流程圖對(duì)x86和x86-64架構(gòu)都適用。
圖3 sumOfCubes流程圖
在Figure3中,綠色的菱形代表開(kāi)始點(diǎn),紅色矩形代表結(jié)束點(diǎn)。藍(lán)色菱形代表在運(yùn)行時(shí)作為sumOfCubes函數(shù)中一部分而被執(zhí)行的條件。如果處理器支持SSE4并且x大于等于8,就會(huì)使用SSE4指令同時(shí)執(zhí)行四個(gè)乘法指令。同時(shí)把同一操作在多個(gè)值上執(zhí)行的過(guò)程被稱(chēng)為向量化。編譯器也會(huì)將循環(huán)展開(kāi),就是說(shuō)循環(huán)體將會(huì)把每次迭代循環(huán)重復(fù)一次。這樣做的最終效果就是八次乘法在每次迭代都會(huì)被執(zhí)行。當(dāng)x的值小于8時(shí),傳統(tǒng)的指令將會(huì)被用于執(zhí)行余下的運(yùn)算。注意到編譯器放出了結(jié)合了三個(gè)獨(dú)立結(jié)尾的循環(huán)結(jié)束點(diǎn)而不是一個(gè)。這將會(huì)減少跳轉(zhuǎn)次數(shù)。
循環(huán)展開(kāi)是重復(fù)執(zhí)行循環(huán)體的過(guò)程,展開(kāi)后的循環(huán)每次把未展開(kāi)循環(huán)內(nèi)的循環(huán)體執(zhí)行不止一次。這樣做的原因是可以通過(guò)減少循環(huán)控制指令的執(zhí)行頻率來(lái)提升性能。也許更重要的是,這樣可以允許編譯器進(jìn)行許多其他優(yōu)化工作,比如向量化。循環(huán)展開(kāi)的弊端是會(huì)增加代碼量和寄存器的壓力。但是這可能使性能達(dá)到兩位百分?jǐn)?shù)級(jí)別的提升,當(dāng)然這是和具體的循環(huán)體有關(guān)的。
不同于x86處理器,所有的x86-64處理器都支持SSE2.不僅如此,你可以在最新的x86-64微處理器架構(gòu)上(包括Intel和AMD)通過(guò)打開(kāi)/arch開(kāi)關(guān)來(lái)利用AVX/AVX2指令集。打開(kāi)/architecture:AVX2也會(huì)允許編譯器使用FMA和BMI指令集。
當(dāng)前的Visual C++編譯器不支持控制循環(huán)展開(kāi)。但是你可以通過(guò)使用模版結(jié)合__ forceinline關(guān)鍵字來(lái)模仿這一技術(shù)。你可以通過(guò)使用no_vector選項(xiàng)來(lái)禁用對(duì)于某個(gè)函數(shù)的自動(dòng)向量化。
通過(guò)觀察生成的匯編代碼,如果你有足夠敏銳的眼睛的話(huà)你會(huì)注意到代碼還有少許優(yōu)化空間。但是,編譯器已經(jīng)做了很多工作了,并且不會(huì)再花更多的時(shí)間分析代碼和進(jìn)行一些無(wú)關(guān)緊要的優(yōu)化。
SumOfCubes(原文是someOfCubes,應(yīng)該是寫(xiě)錯(cuò)了——譯者注)不是唯一一個(gè)循環(huán)被展開(kāi)的函數(shù)。如果你修改代碼讓m作為參數(shù)而不是n,編譯器將不能對(duì)代碼進(jìn)行估計(jì),因此必須放出其代碼。在這種情況下,循環(huán)被展開(kāi)了兩次。
最后我要討論的優(yōu)化是循環(huán)不變量代碼移動(dòng)(loop-invariant code motion)??紤]如下代碼:
int sum(int x) {
int result = 0;
int count = 0;
for (int i = 1; i <= x; ++i) {
++count;
result += i;
}
printf("%d", count);
return result;
}
這里唯一的改變是增加了一個(gè)變量并且在每次循環(huán)進(jìn)行自增,然后打印。不難看出這段代碼可以通過(guò)把變量count的自增移出循環(huán)來(lái)優(yōu)化。也就是說(shuō),我可以直接把x的值賦給變量count。這種優(yōu)化被稱(chēng)為循環(huán)不變量代碼移動(dòng)(loop-invariant code motion)。循環(huán)不變量部分清楚的表明這項(xiàng)技術(shù)只能用于其代碼不依賴(lài)于任何循環(huán)之前的表達(dá)式的情況。
那么這里有一個(gè)問(wèn)題:如果你自己來(lái)進(jìn)行這項(xiàng)優(yōu)化,生成的代碼可能在某些情況下會(huì)導(dǎo)致性能下降。能發(fā)現(xiàn)為什么嗎?考慮x為非正數(shù)的情況。循環(huán)將不被執(zhí)行,這意味著未被手動(dòng)優(yōu)化的代碼中count不會(huì)被訪問(wèn)。但是,在我們手動(dòng)優(yōu)化過(guò)的代碼中在循環(huán)外進(jìn)行了一次不必要的賦值操作,把x賦給了count。更甚者,如果x是負(fù)數(shù),count就會(huì)擁有錯(cuò)誤的值。程序員和編譯器都容易受到這種陷阱的影響。所幸Visual C++編譯器足夠聰明地在賦值之前加上了循環(huán)條件,這樣可以對(duì)所有x的值都生成性能有所提升的代碼。
綜上所述,如果你既不是編譯器也不是編譯器優(yōu)化方面的專(zhuān)家,你應(yīng)該避免僅僅因?yàn)橄胱尨a更快而進(jìn)行手工修改。管住你的手并且相信編譯器將會(huì)優(yōu)化你的代碼。
控制優(yōu)化
除了/O1,/O2,和/Ox編譯開(kāi)關(guān),你還可以使用控制優(yōu)化編譯來(lái)達(dá)到讓某個(gè)函數(shù)優(yōu)化的目的,其形式如下:
#pragma optimize( "[optimization-list]", {on | off} )
[optimization-list]可以為空或者一個(gè)或多個(gè)緊跟的值:g,s,t和y。分別對(duì)應(yīng)編譯器開(kāi)關(guān)/Og,/Os,/Ot和/Oy.
空列表和off參數(shù)會(huì)讓所有的優(yōu)化都被關(guān)閉,不管之前的編譯器開(kāi)關(guān)是否被打開(kāi)。空列表和on參數(shù)會(huì)讓之前打開(kāi)的編譯器開(kāi)關(guān)生效。
/Og開(kāi)關(guān)啟用全局優(yōu)化,全局優(yōu)化只作用域那些通過(guò)表面分析就可以被優(yōu)化的函數(shù)上,而這些函數(shù)內(nèi)部調(diào)用的其他函數(shù)則不會(huì)被優(yōu)化。如果(鏈接時(shí)代碼生成)LTCG被啟用,/Og允許代碼全局優(yōu)化(WPO)。
當(dāng)你需要讓不同的函數(shù)進(jìn)行不同的優(yōu)化時(shí),比如一些進(jìn)行空間尺寸優(yōu)化而另一些進(jìn)行執(zhí)行速度優(yōu)化,那么優(yōu)化編譯參數(shù)就很有用了。但是如果真的想達(dá)到那種粒度的控制,你應(yīng)該考慮性能分析引導(dǎo)優(yōu)化(PGO),就是通過(guò)對(duì)運(yùn)行測(cè)量代碼時(shí)的行為信息進(jìn)行記錄,然后使用這一紀(jì)錄對(duì)代碼進(jìn)行優(yōu)化的過(guò)程。編譯器使用性能分析來(lái)決定怎樣優(yōu)化代碼。Visual Studio提供了必要的工具,來(lái)將這一技術(shù)同時(shí)應(yīng)用于本機(jī)代碼和托管代碼上。
.NET中的優(yōu)化
在.NET的編譯模型中沒(méi)有鏈接器。但是有一個(gè)源代碼編譯器(C# compiler)和即時(shí)編譯器(JIT compiler),源代碼編譯器只進(jìn)行很小的一部分優(yōu)化。比如它不會(huì)執(zhí)行函數(shù)內(nèi)聯(lián)和循環(huán)優(yōu)化。而這些優(yōu)化是由即時(shí)編譯器執(zhí)行的。在4.5以前的所有.NET Framework JIT都不支持SIMD指令集。但是.NET Framework 4.5.1和之后的版本都裝有支持SIMD的即時(shí)編譯器,被稱(chēng)為RyuJIT。
從優(yōu)化能力上來(lái)講RyuJIT和Visual C++有什么不同呢?因?yàn)镽yuJIT是在運(yùn)行時(shí)完成其工作的,所以它可以完成一些Visual C++不能完成的工作。比如在運(yùn)行時(shí),RyuJIT可能會(huì)判定,在這次程序的運(yùn)行中一個(gè)if語(yǔ)句的條件永遠(yuǎn)不會(huì)為true,所以就可以將它移除。RyuJIT也可以利用他所運(yùn)行的處理器的能力。比如如果處理器支持SSE4.1,即時(shí)編譯器就會(huì)只寫(xiě)出sumOfCubes函數(shù)的SSE4.1指令,讓生成打的代碼更加緊湊。但是它不能花更多的時(shí)間來(lái)優(yōu)化代碼,因?yàn)榧磿r(shí)編譯所花的時(shí)間會(huì)影響到程序的性能。另一方面,Visual C++編譯器可以花更多的時(shí)間尋找和利用更多恰當(dāng)?shù)膬?yōu)化機(jī)會(huì)。微軟新推出了一項(xiàng)稱(chēng)為.NET Native的全新技術(shù),允許你使用Visual C++編譯器后端對(duì)托管代碼(Managed Code)進(jìn)行編譯和優(yōu)化,并形成自包含的獨(dú)立可執(zhí)行程序。當(dāng)下這項(xiàng)技術(shù)只支持Windows Store apps。
在當(dāng)前控制托管代碼的能力是很有限的。C#和VB編譯器只允許使用/optimize編譯器開(kāi)關(guān)打開(kāi)或者關(guān)閉優(yōu)化功能。為了控制即時(shí)編譯優(yōu)化,你可以在方法上使用System.Runtime.CompilerServices.MethodImpl屬性和MethodImplOptions中指定的選項(xiàng)。NoOptimization選項(xiàng)可以關(guān)閉優(yōu)化,NoInlining阻止方法被內(nèi)聯(lián),AggressiveInlining (.NET 4.5)選項(xiàng)推薦(不僅僅是提示)即時(shí)編譯器將一個(gè)方法內(nèi)聯(lián)。
結(jié)語(yǔ)
本文中提到的所有優(yōu)化功能都會(huì)顯著地將你的代碼效率提升兩位百分?jǐn)?shù)級(jí)別,并且Visual C++編譯器支持所有這些優(yōu)化。重要的是這些技術(shù)能夠在應(yīng)用之后,帶來(lái)其他更多的優(yōu)化。本文絕不敢奢望能夠?qū)isual C++編譯器的優(yōu)化工作進(jìn)行一次綜合全面的討論。但是我希望通過(guò)本文可以讓你領(lǐng)會(huì)編譯器的精妙。Visual C++可以做比這多得多的事情,所以敬請(qǐng)期待Part2。
更多信息請(qǐng)查看IT技術(shù)專(zhuān)欄