每個程序員都應(yīng)當(dāng)知道的編譯器優(yōu)化知識
來源:易賢網(wǎng) 閱讀:2179 次 日期:2015-04-02 14:00:10
溫馨提示:易賢網(wǎng)小編為您整理了“每個程序員都應(yīng)當(dāng)知道的編譯器優(yōu)化知識”,方便廣大網(wǎng)友查閱!

高級編程語言提供的函數(shù)、條件語句和循環(huán)這樣的抽象編程構(gòu)造極大地提高了編程效率。然而,這也潛在地使性能顯著下降成為了用高級編程語言寫程序的一大劣勢。在理想條件下,在不以性能為妥協(xié)的情況下,你應(yīng)該寫出易讀并且易維護的代碼。因此,編譯器嘗試自動優(yōu)化代碼以提高其性能,當(dāng)今的編譯器都深諳其道。編譯器可以轉(zhuǎn)化循環(huán)、條件語句和遞歸函數(shù)、消除整塊代碼和利用目標(biāo)指令集的優(yōu)勢讓代碼變得高效而簡潔。所以對程序員來說,寫出可讀性高的代碼要比因為手工優(yōu)化而使代碼變得神秘且難以維護更加可貴。事實上,手工優(yōu)化的代碼反而可能會讓編譯器難以進行額外和更加有效的優(yōu)化。

比起手工優(yōu)化代碼,你更應(yīng)該考慮關(guān)于設(shè)計的各個方面,比如使用更快的算法,引入線程級并行機制和利用框架特性(比如move構(gòu)造函數(shù))。

這篇文章是關(guān)于Visual C++ 編譯器優(yōu)化的。為了便于應(yīng)用,我將會討論編譯器采取的最重要的優(yōu)化技巧和決策。我的目的不是告訴你如何手工優(yōu)化代碼,而是向你展示為什么你可以信賴編譯器來優(yōu)化你寫出的代碼。這篇文件絕不是對Visual C++ 編譯器優(yōu)化工作的全面考察。但是將會給你展示那些你真正想要了解的優(yōu)化工作和怎樣與你的編譯器溝通來應(yīng)用它們。

有一些重要的優(yōu)化是超出所有現(xiàn)有編譯器能力的——比如,用高效的算法代替低效的,或者改變數(shù)據(jù)結(jié)構(gòu)的排列以優(yōu)化其在內(nèi)存中的布局。但是這些優(yōu)化話題超出了本文的范圍。

定義編譯器優(yōu)化

優(yōu)化工作涉及到的一個方面,是把一行代碼轉(zhuǎn)化成同等效果的另一行代碼,在這個過程中提升它的一項或多項性能。最重要的兩項性能(指標(biāo))是代碼的執(zhí)行速度和長度。其他一些特性包括代碼執(zhí)行開銷,代碼編譯所需時間,如果代碼需要通過即時編譯機制(Just-in-Time (JIT))進行編譯,那么JIT所需的編譯時間也是指標(biāo)之一。

編譯器經(jīng)常會依據(jù)它們所使用的技術(shù)優(yōu)化代碼。雖然并不完美,但是比起花時間手工苦苦推敲一個程序,利用編譯器提供的特有功能和讓編譯器來優(yōu)化代碼要高效得多。

這里有4種方法讓你的編譯器更加高效地優(yōu)化代碼:

書寫可讀、高效的代碼。不要把Visual C++ 面向?qū)ο蟮奶匦援?dāng)作性能的敵人。最新版本的C++可以讓這些開銷保持到最低甚至消除這些開銷。

2.使用編譯器聲明。例如讓編譯器使用比默認情況更快的函數(shù)調(diào)用約定。

3.使用編譯器內(nèi)置函數(shù)(compiler-intrinsic functions)。內(nèi)在函數(shù)是其實現(xiàn)由編譯器自動提供的特殊函數(shù)。編譯器對其很熟悉并且會用極其高效的指令序列來代替函數(shù)調(diào)用,以充分利用目標(biāo)指令集的優(yōu)勢。當(dāng)前Microsoft .NET Framework不支持編譯器內(nèi)置函數(shù),因此其下的語言都不支持。但是Visual C++ 對這一特性有外在支持。注意,雖然使用內(nèi)置函數(shù)能夠提升代碼性能,但是會降低可讀性和可移植性。

4. 使用性能分析引導(dǎo)優(yōu)化(profile-guided optimization)。使用這一技術(shù),可以讓編譯器搜集更多關(guān)于代碼的運行時行為,并且以此來作為優(yōu)化依據(jù)。

本文的目的是通過證明編譯器可以在低效但是可讀性強的代碼上應(yīng)用優(yōu)化(應(yīng)用第一條方法),從而向你展示為什么你可以信任編譯器。當(dāng)然我也會提供一些對性能分析引導(dǎo)優(yōu)化(profile-guided optimization)的簡短說明,和提到一些可以微調(diào)代碼的編譯器聲明。

編譯器有許多優(yōu)化技巧,從像常量折疊這樣簡單的變換,直到像指令重排(instruction scheduling)這樣極其復(fù)雜的變換。然而在這篇文章中我只有限地討論了一些最重要的優(yōu)化——那些可以顯著地提升性能(兩位數(shù)的百分?jǐn)?shù)來衡量)和減少代碼長度的優(yōu)化:內(nèi)聯(lián)函數(shù)(function inlining)、COMDAT優(yōu)化(COMDAT optimizations)和循環(huán)優(yōu)化。我將會在下一部分討論前兩個話題,然后展示你如何控制Visual C++實現(xiàn)優(yōu)化。最后會有.NET Framework優(yōu)化的簡略說明。通篇我都將會采用Visual Studio 2013來構(gòu)建代碼。

鏈接時代碼生成

鏈接時代碼生成(LTCG)是一項應(yīng)用在C/C++代碼上的程序全局優(yōu)化(WPO)技術(shù)。C/C++編譯器獨立地編譯每個源文件然后產(chǎn)生出相應(yīng)的目標(biāo)文件。這意味著編譯器只能在單個源文件上應(yīng)用優(yōu)化技術(shù),而無法照顧到整個程序。但是,一些重要的優(yōu)化卻只能瀏覽全部程序后才能產(chǎn)生。所以你只能在鏈接時(link time)應(yīng)用這些優(yōu)化,而非編譯時(compile time),因為鏈接器可以完整地看到程序。

當(dāng)LTGC被打開時(通過指定編譯器開關(guān)/GL),編譯器驅(qū)動程序(cl.exe)將只調(diào)用編譯器前端(c1.dll or c1xx.dll),并把后端調(diào)用(c2.dll)推遲到鏈接時間。產(chǎn)出的目標(biāo)文件包含通用中間語言(Common Intermediate Language——CIL)代碼,而不是依賴機器的匯編代碼。然后,當(dāng)鏈接器(link.exe)被調(diào)用,它就能看到包含C中間語言的目標(biāo)文件,并調(diào)用編譯器后端,依次進行程序全局優(yōu)化,生成二進制目標(biāo)文件,再返回鏈接器把所有目標(biāo)文件鏈接在一起,最后生成可執(zhí)行文件。

編譯器前端實際上進行了一些優(yōu)化,比如無論優(yōu)化啟用還是禁用,都會進行常量折疊。但是所有重要的優(yōu)化工作都是在編譯器后端進行的,并且可以使用編譯器開關(guān)控制。

鏈接時代碼生成(LTCG)能讓后端積極地執(zhí)行許多優(yōu)化(通過指定/GL與/O1或/O2,以及/Gw編譯器開關(guān),和/OPT:REF 與 /OPT:ICF鏈接器開關(guān))。在本文中,討論僅限于內(nèi)聯(lián)函數(shù)(function inlining)和COMDAT優(yōu)化(COMDAT optimizations)。關(guān)于完整的鏈接時代碼生成優(yōu)化,請參考相關(guān)文檔。注意鏈接器可以在本地目標(biāo)文件,本地/托管混合目標(biāo)文件,純托管目標(biāo)文件,安全托管目標(biāo)文件和安全.net模塊上執(zhí)行鏈接時代碼生成。

我編寫了一個包含兩個源文件(source1.c 和 source2.c)和一個頭文件(source2.h)的程序。source1.c 和 source2.c分別在Figure 1 and Figure 2中。由于頭文件中非常簡單地包含了source2.c中的函數(shù)原型, 所以并沒有列出。

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文件包含兩個函數(shù),有一個參數(shù)并返回這個參數(shù)的平方的square函數(shù),以及程序的main函數(shù)。main函數(shù)調(diào)用source2.c中除了isPrime之外的所有函數(shù)。source2.c有5個函數(shù)。cube返回一個數(shù)的三次方;sum函數(shù)返回從1到給定數(shù)的和;sumOfcubes返回1到給定數(shù)的三次方的和;isPrime用于判斷一個數(shù)是否是質(zhì)數(shù);getPrime函數(shù)返回第x個質(zhì)數(shù)。我省略掉了容錯處理因為那并非本文的重點。

這些代碼簡單但是很有用。其中一些函數(shù)只進行簡單的運算,一些需要簡單的循環(huán)。getPrime是當(dāng)中最復(fù)雜的函數(shù),包含一個while循環(huán)且在循環(huán)內(nèi)部調(diào)用了也包含一個循環(huán)的isPrime函數(shù)。我將會利用這些函數(shù)證實被稱作內(nèi)聯(lián)函數(shù)的優(yōu)化,和一些其他的優(yōu)化,其中內(nèi)聯(lián)函數(shù)這是編譯器最重要的優(yōu)化之一。

我會在三種不同的配置下生成代碼并且檢驗結(jié)果來驗證代碼是如何被編譯器轉(zhuǎn)化的。如果你也照做的話,你需要匯編生成文件(由編譯器開關(guān)/FA[s]生成)來檢驗生成的匯編代碼以及映像文件(由鏈接器開關(guān)/MAP生成)來檢驗初始化數(shù)據(jù)優(yōu)化是否被執(zhí)行(如果你指定了/verbose:icf 和 /verbose:ref開關(guān),鏈接器也可以匯報這一項)。因此你需要確保在接下來的配置中指定了上述開關(guān)。我也會使用C編譯器(/TC)以讓生成的代碼容易檢驗。但是這篇文章中所有我討論的東西對于C++一樣適用。

Debug配置

之所以使用Debug配置,是因為在你打開了編譯器/Od開關(guān)而沒有打開/GL開關(guān)時,所有的后端優(yōu)化都是禁用的。當(dāng)在這項配置下構(gòu)建代碼時,生成的目標(biāo)文件將包含和源代碼完全對應(yīng)的二進制代碼。你可以通過生成的匯編輸出文件和映像文件來確認這一點。這項配置相當(dāng)于Visual Studio中的調(diào)試配置。

編譯時代碼生成Release配置

這項配置和優(yōu)化被啟用的配置(通過指定/O1,/O2或/Ox編譯器開關(guān))非常相似,但是不指定/GL編譯器開關(guān)。在這項配置下,生成目標(biāo)文件將包含優(yōu)化過的二進制代碼。但是沒有整個程序級別的優(yōu)化。

通過查看source1.c生成的匯編代碼文件,你會看到執(zhí)行了兩項優(yōu)化。首先,通過在編譯時的評估計算把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ù)之間沒有發(fā)生改變。因此,編譯器總結(jié)出執(zhí)行乘法和用25替代結(jié)果是安全的。第二項優(yōu)化,對于square的第二次調(diào)用square(m),也被當(dāng)作內(nèi)聯(lián)函數(shù)。但是,因為m的值在編譯時是未知的,所以編譯器不能對計算估值,所以事實上代碼被保留了。

現(xiàn)在我會檢查source2.c的匯編代碼文件,這將會更有趣。在函數(shù)sumOfCubes內(nèi)對cube的調(diào)用被作為內(nèi)聯(lián)函數(shù)。這會讓編譯器啟用了對循環(huán)來說意義重大的一些優(yōu)化(如你在“循環(huán)優(yōu)化”部分將看到的)。此外,SSE2指令集被用于在isPrime函數(shù)中,當(dāng)調(diào)用了sqrt函數(shù)時把int轉(zhuǎn)化為double而在sqrt返回值時又把double轉(zhuǎn)化為int。并且sqrt只在循環(huán)開始前調(diào)用了一次。注意如果/arch編譯器開關(guān)沒有被打開,x86編譯器將會默認使用SSE2。大多數(shù)x86處理器以及所有x86-64處理器,都支持SSE2。

鏈接時代碼生成Release配置

鏈接時代碼生成(LTCG) Relase配置與Visual Studio中的Release配置相同。在這項配置中,優(yōu)化被啟用并且/GL編譯器開關(guān)被打開。這個開關(guān)隱含的指定了使用/O1或者/O2。這告訴編譯器生成通用中間語言(Common Intermediate Language——CIL)目標(biāo)文件而不是匯編目標(biāo)文件。這樣,鏈接器像之前所說那樣調(diào)用編譯器的后端來執(zhí)行整個程序的優(yōu)化。現(xiàn)在我將會討論一些程序全局優(yōu)化來展示鏈接時代碼生成帶來的巨大好處。這項配置所生成的匯編代碼列表可以在網(wǎng)絡(luò)上得到。

只要允許函數(shù)被內(nèi)聯(lián)(/Ob控制,不論何時,只要需要優(yōu)化就可以打開),不論/Gy開關(guān)(稍后討論)是否打開,/GL開關(guān)都允許把其他翻譯單元中定義的函數(shù)作為內(nèi)聯(lián)函數(shù)。/LTCG鏈接器開關(guān)是可選的并且只為鏈接器提供指導(dǎo)。

通過查看source1.c的匯編代碼,你會看到除了scanf_s之外的所有函數(shù)都被作為了內(nèi)聯(lián)函數(shù)。因此,編譯器被允許執(zhí)行函數(shù)cube,sum和sunOfCubes的計算。只有isPrime函數(shù)沒有被作為內(nèi)聯(lián)函數(shù)。但是,如果它被我們手動在getPrime中寫為內(nèi)聯(lián)函數(shù),編譯器仍然會在main函數(shù)中把getPrime作為內(nèi)聯(lián)函數(shù)。

正如你所見,將函數(shù)內(nèi)聯(lián)很重要不僅僅是因為它總是優(yōu)化函數(shù)調(diào)用,而且它可以允許編譯器進行許多其他優(yōu)化。將函數(shù)內(nèi)聯(lián)通常會以代碼量增加為代價來提升性能。過度地使用這一優(yōu)化會導(dǎo)致我們熟知的代碼膨脹現(xiàn)象。在每一次調(diào)用函數(shù)的地方,編譯器都會分析這樣做的利弊來決定是否將一個函數(shù)作為內(nèi)聯(lián)函數(shù)。

由于內(nèi)聯(lián)的重要性,Visual C++編譯器提供了比對內(nèi)聯(lián)的標(biāo)準(zhǔn)規(guī)定控制更多的支持。你可以通過使用auto_inline編譯控制編譯器不將一段范圍內(nèi)的函數(shù)內(nèi)聯(lián)。你可以通過標(biāo)記為__declspec(noinline)控制編譯器不把特定的函數(shù)或方法內(nèi)聯(lián)。你可以用關(guān)鍵字inline標(biāo)記一個函數(shù)來給編譯器提示將這個函數(shù)作為內(nèi)聯(lián)函數(shù)(雖然編譯器可能選擇忽略這一標(biāo)記如果這次內(nèi)聯(lián)帶來的是凈損失)。inline關(guān)鍵字從C++的第一個版本——C99,就可以使用了。你可以同時在C或者C++中使用微軟特有的關(guān)鍵字_inline,這在你使用不支持inline的老式C版本時是很有用的。并且,你可以使用__forceinline關(guān)鍵字(C和C++)來強制編譯器將任何可以內(nèi)聯(lián)的函數(shù)內(nèi)聯(lián)。最后但是很重要的一點是,你可以告訴編譯器以確定或者不確定的深度拆開一個遞歸函數(shù),這可以通過使用inline_recursion編譯指令來達成。注意編譯器當(dāng)下沒有提供任何特性可以讓你在函數(shù)調(diào)用時控制內(nèi)聯(lián),一切都只能在函數(shù)定義時控制。

默認情況下生效的/Ob0開關(guān)會完全禁用內(nèi)聯(lián)功能。你應(yīng)該在調(diào)試代碼時使用這一開關(guān)(它在Visual Studio Debug配置下是自動打開的)。/Ob1開關(guān)讓編譯器只在函數(shù)被定義為inline,__inline 或者__forceinline時,才考慮將函數(shù)內(nèi)聯(lián)。/Ob2開關(guān)在指定了/O[1|2|x]時生效,編譯器將會考慮所有的函數(shù)是否可以內(nèi)聯(lián)。在我看來,只有在/Ob1控制內(nèi)聯(lián)時考慮是否使用inline或_inline才是有意義的。

在一些特定的條件下,編譯器是不能將函數(shù)內(nèi)聯(lián)的。舉個例子,當(dāng)虛調(diào)用一個虛函數(shù)時,因為編譯器不知道哪個函數(shù)將會被調(diào)用,所以這個函數(shù)不能被內(nèi)聯(lián)。另一個例子是當(dāng)通過指針調(diào)用一個函數(shù)而不是通過函數(shù)名時。你應(yīng)該盡力避免這些條件來使得函數(shù)可以被內(nèi)聯(lián)。具體請參考MSDN文檔,那里列出了不能被內(nèi)聯(lián)的完整條件列表。

某些優(yōu)化,當(dāng)其作用于整個程序級別時,往往比其作用于局部時更加有效,函數(shù)內(nèi)聯(lián)就是這種類型的優(yōu)化之一。事實上,大多數(shù)優(yōu)化都在整體級別更加有效。在這一部分余下的內(nèi)容中,我將會討論被稱作COMDAT優(yōu)化的一類特定優(yōu)化。

默認情況下,當(dāng)編譯翻譯單元時,所有的代碼都被存儲到結(jié)果目標(biāo)文件的一個單獨區(qū)塊。鏈接器在單獨區(qū)塊的范疇上進行操作:也就是對這些區(qū)塊進行移除、合并或者重新排序。(但是)這種會妨礙鏈接器進行三項優(yōu)化工作,而這三項優(yōu)化工作對顯著減少可執(zhí)行代碼量和提升性能又非常重要。第一項是消除未被引用的函數(shù)和全局變量;第二項是合并相同的函數(shù)和全局常量;第三項是重新對函數(shù)和全局變量排序,使得那些在同一路徑上執(zhí)行的函數(shù)和被一起訪問的變量在物理內(nèi)存中離得更近,這會讓程序有更好的局部性。

為了能讓這些鏈接器優(yōu)化生效,你可以通過分別打開/Gy(函數(shù)級別鏈接)和/Gw(全局?jǐn)?shù)據(jù)優(yōu)化)來分別讓編譯器對位于在不同區(qū)塊的函數(shù)和變量進行打包操作。這些區(qū)塊被稱為COMDATs。你也可以用__declspec( selectany)標(biāo)記特定的全局?jǐn)?shù)據(jù)變量來告訴編譯器把這個變量加入COMDAT。然后,通過指定/OPT:REF鏈接器開關(guān),鏈接器就會刪去未被引用的函數(shù)和全局變量。你也可以通過指定/OPT:ICF開關(guān),鏈接器就會合并相同的函數(shù)和全局常數(shù)變量。(ICF代表Identical COMDAT Folding。)通過/ORDER鏈接器開關(guān),你可以讓鏈接器把COMDAT以特定的順序放入生成鏡像。注意所有的這些優(yōu)化都是鏈接器優(yōu)化所以不需要/GL開關(guān)。如果是要對程序進行調(diào)試,并且目的明確,那么/OPT:REF和/OPT:ICF開關(guān)應(yīng)當(dāng)關(guān)閉。

你應(yīng)該盡可能使用鏈接時代碼生成(LTCG)。唯一不使用的原因是當(dāng)你想要分發(fā)生成的目標(biāo)文件和二進制文件時。記得這些文件包含通用中間語言(CIL)而不是匯編語言,通用中間語言只能被生成它的特定版本的編譯器和鏈接器識別,這將會明顯限制目標(biāo)文件的使用,因為開發(fā)者必須使用相同版本的編譯器以使用這些文件。這種情況下,除非你愿意為每個版本的編譯器都分發(fā)一份目標(biāo)文件,否則你應(yīng)該使用編譯時代碼生成。除了限制使用,這些目標(biāo)文件通常比相應(yīng)的匯編目標(biāo)文件更加龐大。但是記得CIL目標(biāo)文件帶來的巨大好處,那就是可以進行程序全局優(yōu)化(WPO)。

循環(huán)優(yōu)化

Visual C++支持多種循環(huán)優(yōu)化,但是我只討論其中的3種:循環(huán)展開,自動向量化和循環(huán)不變量代碼移動。如果你修改了Figure1中的代碼讓m代替n作為sumOfCubes的參數(shù),編譯器將不能推斷出參數(shù)的值,所以必須讓函數(shù)可以處理任何參數(shù)。生成函數(shù)被高度優(yōu)化并且尺寸很大,所以編譯器不會將它作為內(nèi)聯(lián)函數(shù)。

用/O1生成匯編代碼,會在空間尺寸上進行優(yōu)化。在這種情況下,不會對sumOfCubes函數(shù)實行任何優(yōu)化操作。用/O2生成代碼針對執(zhí)行速度進行優(yōu)化。生成代碼的長度會很長但是執(zhí)行效率顯著提高,因為sumOfCubes內(nèi)部的循環(huán)被展開并且向量化了。有一個概念很重要,必須理解:如果不把cube函數(shù)內(nèi)聯(lián)就不能進行向量化。而且,不進行內(nèi)聯(lián)的話循環(huán)展開并不會變得高效。Figure3 顯示了生成的匯編代碼的流程圖。這個流程圖對x86和x86-64架構(gòu)都適用。

名單

圖3 sumOfCubes流程圖

在Figure3中,綠色的菱形代表開始點,紅色矩形代表結(jié)束點。藍色菱形代表在運行時作為sumOfCubes函數(shù)中一部分而被執(zhí)行的條件。如果處理器支持SSE4并且x大于等于8,就會使用SSE4指令同時執(zhí)行四個乘法指令。同時把同一操作在多個值上執(zhí)行的過程被稱為向量化。編譯器也會將循環(huán)展開,就是說循環(huán)體將會把每次迭代循環(huán)重復(fù)一次。這樣做的最終效果就是八次乘法在每次迭代都會被執(zhí)行。當(dāng)x的值小于8時,傳統(tǒng)的指令將會被用于執(zhí)行余下的運算。注意到編譯器放出了結(jié)合了三個獨立結(jié)尾的循環(huán)結(jié)束點而不是一個。這將會減少跳轉(zhuǎn)次數(shù)。

循環(huán)展開是重復(fù)執(zhí)行循環(huán)體的過程,展開后的循環(huán)每次把未展開循環(huán)內(nèi)的循環(huán)體執(zhí)行不止一次。這樣做的原因是可以通過減少循環(huán)控制指令的執(zhí)行頻率來提升性能。也許更重要的是,這樣可以允許編譯器進行許多其他優(yōu)化工作,比如向量化。循環(huán)展開的弊端是會增加代碼量和寄存器的壓力。但是這可能使性能達到兩位百分?jǐn)?shù)級別的提升,當(dāng)然這是和具體的循環(huán)體有關(guān)的。

不同于x86處理器,所有的x86-64處理器都支持SSE2.不僅如此,你可以在最新的x86-64微處理器架構(gòu)上(包括Intel和AMD)通過打開/arch開關(guān)來利用AVX/AVX2指令集。打開/architecture:AVX2也會允許編譯器使用FMA和BMI指令集。

當(dāng)前的Visual C++編譯器不支持控制循環(huán)展開。但是你可以通過使用模版結(jié)合__ forceinline關(guān)鍵字來模仿這一技術(shù)。你可以通過使用no_vector選項來禁用對于某個函數(shù)的自動向量化。

通過觀察生成的匯編代碼,如果你有足夠敏銳的眼睛的話你會注意到代碼還有少許優(yōu)化空間。但是,編譯器已經(jīng)做了很多工作了,并且不會再花更多的時間分析代碼和進行一些無關(guān)緊要的優(yōu)化。

SumOfCubes(原文是someOfCubes,應(yīng)該是寫錯了——譯者注)不是唯一一個循環(huán)被展開的函數(shù)。如果你修改代碼讓m作為參數(shù)而不是n,編譯器將不能對代碼進行估計,因此必須放出其代碼。在這種情況下,循環(huán)被展開了兩次。

最后我要討論的優(yōu)化是循環(huán)不變量代碼移動(loop-invariant code motion)??紤]如下代碼:

int sum(int x) {

int result = 0;

int count = 0;

for (int i = 1; i &lt;= x; ++i) {

++count;

result += i;

}

printf("%d", count);

return result;

}

這里唯一的改變是增加了一個變量并且在每次循環(huán)進行自增,然后打印。不難看出這段代碼可以通過把變量count的自增移出循環(huán)來優(yōu)化。也就是說,我可以直接把x的值賦給變量count。這種優(yōu)化被稱為循環(huán)不變量代碼移動(loop-invariant code motion)。循環(huán)不變量部分清楚的表明這項技術(shù)只能用于其代碼不依賴于任何循環(huán)之前的表達式的情況。

那么這里有一個問題:如果你自己來進行這項優(yōu)化,生成的代碼可能在某些情況下會導(dǎo)致性能下降。能發(fā)現(xiàn)為什么嗎?考慮x為非正數(shù)的情況。循環(huán)將不被執(zhí)行,這意味著未被手動優(yōu)化的代碼中count不會被訪問。但是,在我們手動優(yōu)化過的代碼中在循環(huán)外進行了一次不必要的賦值操作,把x賦給了count。更甚者,如果x是負數(shù),count就會擁有錯誤的值。程序員和編譯器都容易受到這種陷阱的影響。所幸Visual C++編譯器足夠聰明地在賦值之前加上了循環(huán)條件,這樣可以對所有x的值都生成性能有所提升的代碼。

綜上所述,如果你既不是編譯器也不是編譯器優(yōu)化方面的專家,你應(yīng)該避免僅僅因為想讓代碼更快而進行手工修改。管住你的手并且相信編譯器將會優(yōu)化你的代碼。

控制優(yōu)化

除了/O1,/O2,和/Ox編譯開關(guān),你還可以使用控制優(yōu)化編譯來達到讓某個函數(shù)優(yōu)化的目的,其形式如下:

#pragma optimize( "[optimization-list]", {on | off} )

[optimization-list]可以為空或者一個或多個緊跟的值:g,s,t和y。分別對應(yīng)編譯器開關(guān)/Og,/Os,/Ot和/Oy.

空列表和off參數(shù)會讓所有的優(yōu)化都被關(guān)閉,不管之前的編譯器開關(guān)是否被打開。空列表和on參數(shù)會讓之前打開的編譯器開關(guān)生效。

/Og開關(guān)啟用全局優(yōu)化,全局優(yōu)化只作用域那些通過表面分析就可以被優(yōu)化的函數(shù)上,而這些函數(shù)內(nèi)部調(diào)用的其他函數(shù)則不會被優(yōu)化。如果(鏈接時代碼生成)LTCG被啟用,/Og允許代碼全局優(yōu)化(WPO)。

當(dāng)你需要讓不同的函數(shù)進行不同的優(yōu)化時,比如一些進行空間尺寸優(yōu)化而另一些進行執(zhí)行速度優(yōu)化,那么優(yōu)化編譯參數(shù)就很有用了。但是如果真的想達到那種粒度的控制,你應(yīng)該考慮性能分析引導(dǎo)優(yōu)化(PGO),就是通過對運行測量代碼時的行為信息進行記錄,然后使用這一紀(jì)錄對代碼進行優(yōu)化的過程。編譯器使用性能分析來決定怎樣優(yōu)化代碼。Visual Studio提供了必要的工具,來將這一技術(shù)同時應(yīng)用于本機代碼和托管代碼上。

.NET中的優(yōu)化

在.NET的編譯模型中沒有鏈接器。但是有一個源代碼編譯器(C# compiler)和即時編譯器(JIT compiler),源代碼編譯器只進行很小的一部分優(yōu)化。比如它不會執(zhí)行函數(shù)內(nèi)聯(lián)和循環(huán)優(yōu)化。而這些優(yōu)化是由即時編譯器執(zhí)行的。在4.5以前的所有.NET Framework JIT都不支持SIMD指令集。但是.NET Framework 4.5.1和之后的版本都裝有支持SIMD的即時編譯器,被稱為RyuJIT。

從優(yōu)化能力上來講RyuJIT和Visual C++有什么不同呢?因為RyuJIT是在運行時完成其工作的,所以它可以完成一些Visual C++不能完成的工作。比如在運行時,RyuJIT可能會判定,在這次程序的運行中一個if語句的條件永遠不會為true,所以就可以將它移除。RyuJIT也可以利用他所運行的處理器的能力。比如如果處理器支持SSE4.1,即時編譯器就會只寫出sumOfCubes函數(shù)的SSE4.1指令,讓生成打的代碼更加緊湊。但是它不能花更多的時間來優(yōu)化代碼,因為即時編譯所花的時間會影響到程序的性能。另一方面,Visual C++編譯器可以花更多的時間尋找和利用更多恰當(dāng)?shù)膬?yōu)化機會。微軟新推出了一項稱為.NET Native的全新技術(shù),允許你使用Visual C++編譯器后端對托管代碼(Managed Code)進行編譯和優(yōu)化,并形成自包含的獨立可執(zhí)行程序。當(dāng)下這項技術(shù)只支持Windows Store apps。

在當(dāng)前控制托管代碼的能力是很有限的。C#和VB編譯器只允許使用/optimize編譯器開關(guān)打開或者關(guān)閉優(yōu)化功能。為了控制即時編譯優(yōu)化,你可以在方法上使用System.Runtime.Compiler­Services.MethodImpl屬性和MethodImplOptions中指定的選項。NoOptimization選項可以關(guān)閉優(yōu)化,NoInlining阻止方法被內(nèi)聯(lián),AggressiveInlining (.NET 4.5)選項推薦(不僅僅是提示)即時編譯器將一個方法內(nèi)聯(lián)。

結(jié)語

本文中提到的所有優(yōu)化功能都會顯著地將你的代碼效率提升兩位百分?jǐn)?shù)級別,并且Visual C++編譯器支持所有這些優(yōu)化。重要的是這些技術(shù)能夠在應(yīng)用之后,帶來其他更多的優(yōu)化。本文絕不敢奢望能夠?qū)isual C++編譯器的優(yōu)化工作進行一次綜合全面的討論。但是我希望通過本文可以讓你領(lǐng)會編譯器的精妙。Visual C++可以做比這多得多的事情,所以敬請期待Part2。

更多信息請查看IT技術(shù)專欄

更多信息請查看技術(shù)文章
由于各方面情況的不斷調(diào)整與變化,易賢網(wǎng)提供的所有考試信息和咨詢回復(fù)僅供參考,敬請考生以權(quán)威部門公布的正式信息和咨詢?yōu)闇?zhǔn)!

2025國考·省考課程試聽報名

  • 報班類型
  • 姓名
  • 手機號
  • 驗證碼
關(guān)于我們 | 聯(lián)系我們 | 人才招聘 | 網(wǎng)站聲明 | 網(wǎng)站幫助 | 非正式的簡要咨詢 | 簡要咨詢須知 | 加入群交流 | 手機站點 | 投訴建議
工業(yè)和信息化部備案號:滇ICP備2023014141號-1 云南省教育廳備案號:云教ICP備0901021 滇公網(wǎng)安備53010202001879號 人力資源服務(wù)許可證:(云)人服證字(2023)第0102001523號
云南網(wǎng)警備案專用圖標(biāo)
聯(lián)系電話:0871-65099533/13759567129 獲取招聘考試信息及咨詢關(guān)注公眾號:hfpxwx
咨詢QQ:526150442(9:00—18:00)版權(quán)所有:易賢網(wǎng)
云南網(wǎng)警報警專用圖標(biāo)