軟件開發者們在開發產品級代碼時常會面對一個艱難的選擇,你總是希望你的代碼性能優越,這意味著你 需要在高優化級別上編譯它;同時,你可能希望調試你加入產品中的這份二進制代碼,而不是編譯時沒有經過 優化的源文件。如果你嘗試過調試優化過的代碼,你可能已經知道這其中的難處了:
源代碼語句不按順序執行,或者在你希望它們執行的時候它們沒有;
變量沒有按預期地進行更新;
變量沒有定義的值,甚至沒有一個定義的標識;
在調試器內對變量的更新對程序執行不起作用 。
這不是因為編譯器出了什麼差錯,它設計的初衷就是為了保留你程序的結果和外部行為,而不是它 在調試器中的瞬態和內部行為。
新一代編譯器
在2012年年中IBM發布了IBMPower系統上C/C++和 Fortran編譯器的最新版本:XLC/C++編譯器V12.1和XLFortran編譯器V14.1,均對應AIX和PowerLinux平台。這 些新的編譯器提供了一系列數值的選擇以供調試優化過的代碼,你可以自由選擇在完全優化代碼和完全可調試 代碼之間的權衡級別。在大多數其它編譯器中,開發者有兩個可用的選擇:
編譯一個沒有優化的調試版本 (例如使用-g),或
一個優化過的版本,但是可調試性較差(例如使用-O2)。
在新的XLC++ V12.1和XL FortranV14.1編譯器中,你可以使用一系列-g的值,從-g0一直到-g9,調試級別越低,在調試期間 觀測的錯誤可能性越大,這意味著你可以自由權衡可調試性和性能。在程序優化時編譯器會保障各級別的可調 試性,並在調試過程中在保證預期行為的情況下有效地將應用的性能最大化。
執行順序
編譯器 在優化過程中經常會在不改變程序結果的前提下重新編排邏輯,編譯器這麼做可能是為了直接提高程序的性能 ,或者是為了讓接下來的優化能提高程序的性能。此外,為了提高處理器吞吐量或是其它的原因,當主優化階 段完成、指令生成時,源代碼行相關的指令序列可能也會被重新排序,
這些編譯器的優化有兩個主要 的影響:第一,如果你按源代碼的行數單步跟蹤程序,那麼程序可能不會按照正確的順序執行。第二,如果你 在一個過程的開始設了個斷點,沒有人能保證這過程的參數在這時會含有正確的值,程序甚至根本不會進入這 個過程。這是因為編譯器在優化過程中將被調用的過程代碼內聯至調用處。在XLC/C++和 XLFortran的早先版 本中,舊的-g行為會生成全部調試信息,但它對於一個優化過的程序是否有用就不得而知了,因此,在用-g和 -O編譯的程序中,當你試圖去調試並觀測某個特定過程被調用時的參數是什麼時,你甚至不能確定過程入口處 設立的斷點是否可以讓你看到這些參數的實際值。然而,-g3或更高的調試級別可以保證在過程的入口處參數 是有效且可見的。
優化代碼中的某些語句可能並沒有被執行到,而另一些可能比在它們之前的語句更先被 執行。如果你在一個給定的源代碼行中設立一個斷點,你不能確定邏輯上在這之前的行中變量被賦的值是否正 確。例如,考慮一下程序段:
10 x=x+1;
11fl=fl1/fl2;
12 y=y+1;
如果我們在第11行設立一個斷點並運行程序,調試器會在與第11行關聯的第一條指令處 停下,但此時第10行可能還沒有被執行過,因為第10行與第11行是無關的,編譯器可以自由將它們調換順序。 如果你想要單步執行這段代碼,你可能會發現我們在停在第10行前就已經執行到了11行,這是一個很強的信號 (但不是一定的),說明與第10行相關的指令都還沒有被執行到。 在這個例子中編譯器會將除法盡早做好, 因為除法指令有很長的延遲,而此時一個先進的流水線式處理器可以在除法進行時繼續工作,這樣的話沒有關 聯上的行為,例如將x的值讀入寄存器並將它加1,可能會同時進行。假設我們對在第11行處的除法結果有興趣 。在沒有優化過的程序中,我們只需要單步到第12行,再查看fl的值。但在優化過的程序中,單步到第12步不 能保證所有與第11行相關的操作都已經完成,我們可能已經完成了除法但還沒有將結果存回內存,因此,確定 變量已經可以顯示正確結果的唯一方法是單步執行機器指令直到你看到除法運算的結果被寫回了內存空間。如 果你只關心結果的值(而不是驗證是否更新了變量),取而代之你可以在除法運算更新了其目標寄存器的同時 打印它的值。
當你用 XLC/C++ V12.1或 XL FortranV14.1的-g8或更高的選項編譯程序時,調試器可以 得到每個可執行語句開始時程序的狀態。在先前的例子上使用-g8可以確保當你到達第11行的斷點時,第10行 的加法已經完成,而且你可以通過調試器來得到其結果。
被移除的變量
調試器總是認為變量在 內存中,因此當你在調試器中檢查變量時,它會打印出程序分配給該變量的內存位置中的內容。然而,編譯器 在優化過程中總是盡可能在它認為安全的情況下避免更新與變量相關聯的內存地址,這麼設計是為了提高性能 ,因為執行讀取與存儲的代價非常大。如果編譯器可以在變量被修改後將它保存在寄存器中,那麼它就不會把 它立即寫回內存裡,甚至永遠都不會寫。這麼做的副作用是相當於在調試器面前將變量隱藏了起來。考慮一個 標准的索引變量ii,請看如下例子:
20 for (ii=0; ii<10; i++)
21 sum=sum+array[ii]; 在這個例子中,大多數編譯器在編譯時會將 ii和sum保存在寄存器裡。如果在循環完成後要使用ii的話,編譯器會再生成代碼來將ii寫回到內存位置,但 大多數情況下,如果寄存器中存放的值再也沒用了的話就會被捨棄(覆寫)。這可能會使得調試過程變得復雜 :
調試器辨認不出變量。當你試圖打印、查看ii時,調試器不知道它是什麼;
調試器只能提供ii的 類型和上下文信息,但不能顯示它的值;
調試器可以顯示賦給ii的內存位置的值,但這個值已經過期 或無用了。
同樣,新的-g8或-g9選項可以解決這些與被移除的變量或未知的變量值相關的問題。
改變變量的值
在程序運行時用調試器改變變量的值來改變程序的行為是另一種調試應用的技術 。顯然,如果編譯器將變量的引用優化掉或是使得調試器找不到變量在機器級的位置,那麼這技術也就沒法使 用了,甚至更糟的情況是它貌似有用,但是卻沒有得到預期的效果。
這是-g8與-g9之間的主要區別。對於 -g8,編譯器提供了程序在每條可執行語句開始處的狀態,但不保證編程人員可以在調試時修改變量的值。另 一方便,-g9使得你可以修改程序變量並讓此修改影響程序的執行。顯而易見,編譯器在生成代碼時必須保證 各程序變量能在調試時被修改,因此在上述循環的例子中,在每個循環的迭代裡ii都應該能從內存中被讀取到 。這會大幅度地影響性能,因此只有在你覺得上述特質對你維護代碼至關重要時才應該在產品級的代碼上使用 -g9。在絕大多數情況下,檢測程序時你完全不需要這麼做就可以理解程序的行為並找出潛在的漏洞,這時, 我們更推薦使用-g8。
內聯和經優化的調試
編譯器在優化時經常將被調用的過程“內聯”至調 用的地方,換句話說,他們用被調用的過程體的拷貝來取代調用操作。這麼做是為了消除調用的代價,同時, 那些依賴於調用處上下文的優化可以作用到內聯的代碼上。然而,內聯會使得調試變得更加困難。
當 一個過程被內聯後,不再存在調用指令。大多數調試器都提供“Step”命令和“next”命令,這兩者在面對語 句時行為是一致的,但在面對調用時不同,“Step”進入被調用的過程的第一條語句,而“Next”會跨過(執 行完)調用(除非在執行被調用過程時遇到了斷點)。 由於當過程內聯時不再有調用指令,Step和Next命令 對於一個內聯過程的“調用”具有相同的效果,如果沒有帶擴展功能的調試器支持的話,兩條命令都會執行內 聯函數的代碼,就好像用戶正步入調用一樣。
在XL編譯器中,高調試級別在處理內聯上相較於早先的產品 有了巨大的進步。在-g8或更高的級別中,編譯器會在每個內聯代碼的語句中插入狀態信息,使得你可以在調 試一個內聯過程的調用時單步執行被調用過程的各語句、查看到正確的變量值(就算它依賴於正確的作用域信 息)、甚至在內聯過程中改變本地變量的值(-g9)。然而,“Next”命令不會跨過內聯過程,而是像“Step ”那樣步入內聯過程的代碼。
要注意的是,當你單步運行內聯函數中的語句且當調試器可以跟蹤到運行程 序的內聯過程時,該過程不會出現在調試器的調用棧上。
調試選項
請記住,當你提高經優化的調試級別,你增加了可調試性,但潛在地降低了編譯完後程序 的性能。選擇一個符合你調試需求的調試級別是非常重要的,你需要最小化調試對性能的影響。在很多情況下 ,相較於未做優化的二進制代碼,即使在高調試級別下編譯器還是可以將性能的大幅度增強。例如,一個用- O2-g8編譯的典型程序的運行速度是-O2編譯出的相同程序的80%,對於當需要可調試性時只用-g編譯的傳統方 法來說,這是一個實質性的進步。以下是對各個-g級別效果的簡單說明:
-g0:沒有調試信息。這是缺 省設置。
-g1:生成只含有行號和源文件名字的調試信息,沒有符號表信息。
-g2:“傳統的” -g行為。在優化時,編譯器生成所有調試數據,但是不保證其可用性和准確性。所有之前提到過的問題都有可 能發生。
-g3,-g4:類似-g2,另外,調試器可以在過程開始處查看到函數的參數值。
-g5,- g6,-g7:類似-g3,另外,在循環前後、分支語句、過程調用和各過程的第一句可執行語句中,調試器可以查 看到程序的狀態。
-g8:類似-g3,另外,在每條源代碼行中調試器都能看到程序的狀態。
-g9 :類似-g8,另外,用戶可以在調試器中修改用戶變量來確實地改變程序行為。
使用優化調試的好處
許多 XLC/C++和 XLFortran編譯器的用戶為他們的產品二進制碼創建單獨的調試和優化版本,這是因 為他們考慮到封裝調試版本給性能帶來的影響和代碼經優化後可調試性上的問題。我們可以將優化版本封裝起 來交給客戶,同時用調試版本來解決實際使用時發現的問題。雖然這是一個不錯的方法,但一些開發商更希望 (甚至要求)它們的產品在生產環境中具有完全的可調試性。
我們之所以支持將可調試的二進制代碼 封裝進產品交給客戶,是因為這確保當遇到漏洞時,客戶的運行環境與技術人員在調查該漏洞時的調試環境是 一樣的。優化過的代碼與未優化的代碼之間總是可能會存在行為上的差異,也存在優化過程自身錯誤地改變程 序行為的罕見情況。封裝可調試的二進制代碼可以解決這些問題,尤其是對於那些重視可重現性的開發商。
有了擁有新優化調試支持的Power系統下AIX和Linux平台的XLC/C++和 XLFortran編譯器,你可以同時擁有 兩項的優勢:一個符合你實際維護需要的可調試級別,和一個縮小完全可調試性和完全優化之間差距的性能級 別。最好的一點是,相比起傳統的僅支持調試而不優化的傳統編譯方法來說,即使現在你使用了最高級別的優 化調試,你仍可以使你的應用得到巨大的性能提升。