關於C++的O2優化


Luogu評測姬的O2優化到底什么,為什么其他的OJ上沒有?

首先看一下G++源碼的各種編譯指令

-fthread-jumps 
-falign-functions  
-falign-jumps 
-falign-loops 
-falign-labels 
-fcaller-saves 
-fcrossjumping 
-fcse-follow-jumps  
-fcse-skip-blocks 
-fdelete-null-pointer-checks 
-fdevirtualize 
-fdevirtualize-speculatively 
-fexpensive-optimizations 
-fgcse  
-fgcse-lm  
-fhoist-adjacent-loads 
-finline-small-functions 
-findirect-inlining 
-fipa-cp 
-fipa-cp-alignment 
-fipa-bit-cp 
-fipa-sra 
-fipa-icf 
-fisolate-erroneous-paths-dereference 
-flra-remat 
-foptimize-sibling-calls 
-foptimize-strlen 
-fpartial-inlining 
-fpeephole2 
-freorder-blocks-algorithm=stc 
-freorder-blocks-and-partition -freorder-functions 
-frerun-cse-after-loop  
-fsched-interblock  
-fsched-spec 
-fschedule-insns  
-fschedule-insns2 
-fstrict-aliasing 
-fstrict-overflow 
-ftree-builtin-call-dce 
-ftree-switch-conversion 
-ftree-tail-merge 
-fcode-hoisting 
-ftree-pre 
-ftree-vrp 
-fipa-ra

 

一個C/C++的程序從.c文件到可執行文件,其間經歷了幾步?我們知道: 高級語言是偏向人,按照人的思維方式設計的,機器對這些可是莫名奇妙,不知所謂。那從高級語言是如何過渡到機器語言的呢?這可是一個漫長的旅途。其中,得經歷這樣的歷程:C源程序->編譯預處理->編譯->匯編程序->鏈接程序->可執行文件

從源程序(** . cpp/ ** . c)到可執行文件(** . exe/ **)

  1. 預處理 讀取c源程序,對其中的偽指令(以#開頭的指令)和特殊符號進行處理。偽指令主要包括以下四個方面:

    • (1)宏定義指令,如#define Name TokenString,#undef等。對於前一個偽指令,預編譯所要作得的是將程序中的所有Name用TokenString替換,但作為字符串常量的Name則不被替換。對於后者,則將取消對某個宏的定義,使以后該串的出現不再被替換。
    • (2)條件編譯指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。這些偽指令的引入使得程序員可以通過定義不同的宏來決定編譯程序對哪些代碼進行處理。預編譯程序將根據有關的文件,將那些不必要的代碼過濾掉。
    • (3)加載頭文件,如#include"FileName"或者#include 等。采用頭文件的目的主要是為了使某些定義可以供多個不同的C源程序使用。因為在需要用到這些定義的C源程序中,只需加上一條#include語句即可,而不必再在此文件中將這些定義重復一遍。預編譯程序將把頭文件中的定義統統都加入到它所產生的輸出文件中,以供編譯程序對之進行處理。包含到c源程序中的頭文件可以是系統提供的,這些頭文件一般被放在/usr/include目錄下。在程序中#include它們要使用尖括號(<>)。另外開發人員也可以定義自己的頭文件,這些文件一般與c源程序放在同一目錄下,此時在#include中要用雙引號("")。 預編譯是將.c 文件轉化成 .i文件,   重定向使用的gcc命令是:gcc –E hello.c >hello.i 在預處理階段是不做語法檢查的。
  2. 編譯階段 : 需要進行三個步驟:詞法分析、語法分析和語義分析 在linux環境中,輸入命令:gcc–s hello.c 參數c告訴gcc命令只進行編譯,不做其他處理。命令運行結束后產生hello.o的目標文件。

  3. 匯編過程 編譯過程實際上指把匯編語言代碼翻譯成目標機器指令的過程。對於被翻譯系統處理的每一個C語言源程序,都將最終經過這一處理而得到相應的目標文件。目標文件中所存放的也就是與源程序等效的目標的機器語言代碼。 輸入命令:gcc –c hello.c 就會生成hello.o的目標文件。 4.鏈接過程 鏈接就是將不同部分的代碼和數據收集和組合成為一個單一文件的過程,這個文件可被加載或拷貝到存儲器執行. 鏈接可以執行與編譯時(源代碼被翻譯成機器代碼時),也可以執行與加載時(在程序被 加載器加載到存儲器並執行時),甚至執行與運行時,由應用程序來執行.在現代系統中, 鏈接是由鏈接器自動執行的. 鏈接器分為:靜態鏈接器和動態鏈接器兩種.

 

(1). 靜態鏈接器 靜態鏈接器以一組可重定位目標文件和命令行參數作為輸入,生成一個完全鏈接的可以加載和運行的可執行目標文件作為輸出.

靜態鏈接器主要完成兩個任務:

  • 1>符號解析:目標文件定義和引用符號.符號解析的目的在於將每個符號引用和一個符號定義聯系起來.
  • 2>重定位:編譯器和匯編器生成從地址零開始的代碼和數據節.鏈接器通過把每個符號定義和一個存儲器位置聯系起來,然后修改所有對這些符號的引用,使得他們執行這個存儲位置,從而重定位這些節.

(2)動態鏈接器 共享庫是一個目標模塊,在運行時,可以加載到任意的存儲器地址,並在存儲器中和一個程序鏈接起來.這個過程稱為動態鏈接,是由動態鏈接器完成的. 共享庫的共享在兩個方面有所不同.首先,在任何給定的文件系統中,對於一個庫只有一個.so文件.所有引用該庫德可執行目標文件共享這個.so文件中的代碼和數據,而不是像靜態庫的內容那樣被拷貝和嵌入到引用它們的可執行的文件中.其次,在存儲器中,一個共享庫的.text只有一個副本可以被不同的正在運行的進程共享。

-O1,-O2,-O3為何方神聖,它們是如何優化編譯文件的?

(1)首先,她們的真面目是: -O1 提供基礎級別的優化 -O2提供更加高級的代碼優化,會占用更長的編譯時間 -O3提供最高級的代碼優化 可以使用-f命令行選項引用每個單獨的優化技術。

1. 編譯器優化級別1

在優化的第一個級別執行基礎代碼的優化 這個級別試圖執行9種單獨的優化功能:

(1).-fdefer-pop: 這種優化技術與匯編語言代碼在函數完成時如何進行操作有關。

(2).-fmerge-constans: 使用這種優化技術, 編譯器試圖合並相同的常量.

(3) . -fthread-jumps: 使用這種優化技術與編譯器如何處理匯編代碼中的條件和非條件分支有關。 在某些情況下, 一條跳轉指令可能轉移到另一條分支語句。 通過一連串跳轉, 編譯器確定多個跳轉之間的最終目標並且把第一個跳轉重新定向到最終目標。

(4).-floop-optimize:通過優化如何生成匯編語言中的循環, 編譯器可以在很大程序上提高應用程序的性能。通常, 程序由很多大型且復雜的循環構成。 通過刪除在循環內沒有改變值的變量賦值操作, 可以減少循環內執行指令的數量, 在很大程度上提高性能。 此外優化那些確定何時離開循環的條件分支,以便減少分支的影響。

(5).-fif-conversion: if-then語句應該是應用程序中僅次於循環的最消耗時間的部分。簡單的if-then語句可能在最終的匯編語言代碼中產生眾多的條件分支。 通過減少或者刪除條件分支, 以及使用條件傳送 設置標志和使用運算技巧來替換他們, 編譯器可以減少if-then語句中花費的時間量。

(6)-fif-conversion2: 這種技術結合更加高級的數學特性, 減少實現if-then語句所需的條件分支。

(7)-fdelayed-branch: 這種技術試圖根據指令周期時間重新安排指令。 它還試圖把盡可能多的指令移動到條件分支前, 以便最充分的利用處理器的治理緩存。

(8) -fguess-branch-probability:就像其名稱所暗示的, 這種技術試圖確定條件分支最可能的結果, 並且相應的移動指 令, 這和延遲分支技術類似。因為在編譯時預測代碼的安排,所以使用這一選項兩次編譯相同的c或者c++代碼很可能會產生不同的匯編語言代碼,這取決於編譯時編譯器認為會使用那些分支。

(9)-fcprop-registers: 因為在函數中把寄存器分配給變量, 所以編譯器執行第二次檢查以便減少調度依賴性(兩個段要求使用相同的寄存器)並且刪除不必要的寄存器復制操作。

2 . 編譯器優化級別2

結合了第一個級別的所有優化技術,再加上一下一些優化:

  • (1)-fforce-mem: 這種優化在任何指令使用變量前, 強制把存放再內存位置中的所有變量都復制到寄存器中。 對於只涉及單一指令的變量, 這樣也許不會有很大的優化效果. 但是對於在很多指令(必須數學操作)中都涉及到的變量來說, 這會時很顯著的優化, 因為和訪問內存中的值相比 ,處理器訪問寄存器中的值要快的多。

  • (2)-foptimize-sibling-calls: 這種技術處理相關的和/或者遞歸的函數調用。通常,遞歸的函數調用可以被展開為一系列一般的指令, 而不是使用分支。

  • (3)-fstrength-reduce: 這種優化技術對循環執行優化並且刪除迭代變量。 迭代變量是捆綁到循環計數器的變量, 比如使用變量, 然后使用循環計數器變量執行數學操作的for-next循環。

  • (4)-fgcse: 這些優化操作試圖分析生成的匯編語言代碼並且結合通用片段, 消除冗余的代碼段。如果代碼使用計算性的goto,gcc指令推薦

  • (5)-fcse-follow-jumps: 這種特別的通用子表達式消除技術掃描跳轉指令, 查找程序中通過任何其他途徑都不會到達的目標代碼。這種情況最常見的例子就式if-then-else語句的else部分。

  • (6)-frerun-cse-after-loop: 這種技術在對任何循環已經進行過優化之后重新運行通用子表達式消除例程。這樣確保在展開循環代碼之后更進一步地優化還編代碼。

  • (7)-fdelete-null-pointer-checks: 這種優化技術掃描生成的匯編語言代碼, 查找檢查空指針的代碼。

  • (8)-fextensive-optimizations: 這種技術執行從編譯時的角度來說代價高昂的各種優化技術,但是它可能對運行時的性能產生負面影響。

  • (9)-fregmove: 編譯器試圖重新分配mov指令中使用的寄存器, 並且將其作為其他指令操作數, 以便最大化捆綁的寄存器的數量。

  • (10)-fschedule-insns: 編譯器將試圖重新安排指令, 以便消除等待數據的處理器。對於在進行浮點運算時有延遲的處理器來說, 這使處理器在等待浮點結果時可以加載其他指令。

  • (11)-fsched-interblock: 這種技術使編譯器能夠跨越指令塊調度指令。 這可以非常靈活地移動指令以便等待期間完成的工作最大化。

  • (12)-fcaller-saves: 這個選項指示編譯器對函數調用保存和恢復寄存器, 使函數能夠訪問寄存器值, 而且不必保存和恢復他們。 如果調用多個函數, 這樣能夠節省時間, 因為只進行一次寄存器的保存和恢復操作, 而不是在每個函數調用中都進行。

  • (13)-fpeephole2: 這個選項允許進行任何計算機特定的觀察孔優化。

  • (14)-freorder-blocks: 這種優化技術允許重新安排指令塊以便改進分支操作和代碼局部性。

  • (15)-fstrict-aliasing: 這種技術強制實行高級語言的嚴格變量規則。 對於c和c++程序來說, 它確保不在數據類型之間共享變量. 例如, 整數變量不和單精度浮點變量使用相同的內存位置。

  • (16)-funit-at-a-time:這種優化技術指示編譯器在運行優化例程之前讀取整個匯編語言代碼。這使編譯器可以重新安排不消耗大量時間的代碼以便優化指令緩存。

  • (17)-falign-functions:這個選項用於使函數對准內存中特定邊界的開始位置。大多數處理器按照頁面讀取內存,並且確保全部函數代碼位於單一內存頁面內, 就不需要叫化代碼所需的頁面。

  • (18)-fcrossjumping: 這是對跨越跳轉的轉換代碼處理, 以便組合分散在程序各處的相同代碼。 這樣可以減少代碼的長度,但是也許不會對程序性能有直接影響。

    3. 編譯器優化級別3

     

    它整合了第一和第二級別中的左右優化技巧, 還包括一下優化: -finline-functions:這種優化技術不為函數創建單獨的匯編語言代碼,而是把函數代碼包含在調度程序的 代碼中。 對於多次被調用的函數來說, 為每次函數調用復制函數代碼。 雖然這樣對於減少代碼長度不利, 但是通過最充分的利用指令緩存代碼, 而不是在每次函數調用時進行分支操作, 可以提高性能。 -fweb: 構建用於保存變量的偽寄存器網絡。 偽寄存器包含數據, 就像他們是寄存器一樣, 但是可以使用各種其他優化技術進行優化, 比如cse和loop優化技術。 -fgcse-after-reload:這中技術在完全重新加載生成的且優化后的匯編語言代碼之后執行第二次gcse優化,幫助消除不同優化方式創建的任何冗余段。

     


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM