Release編譯有什麼特點?我的程序用Debug編譯能運行但是用Release卻不行,說明了什麼?


我理解的題目核心應該是問 Release 和 Debug 的區別在哪裡,而不是問如何解決 Release 下不能運行的問題。

Debug 和 Release 並沒有本質的區別,他們只是不同編譯選項的集合,編譯器只是按照具體指定的選項來完成編譯。 Debug 編譯的目的是方便調試程序,Release 編譯的目的是發布版本。

Debug 版本的編譯選項通常包括:

  • 可執行文件中包含調試信息和符號表;
  • 選用 Debug 版本的運行庫進行鏈接;
  • 關閉優化;
  • 打開 DEBUG 宏;
  • 主動捕獲內存錯誤;

Release 版本的編譯選項通常包括:

  • 可執行文件中不包含調試信息和符號表;
  • 選用 Release 版本的運行庫進行鏈接;
  • 啟用一些優化 pass(大概在 O2 或 O3 級別);
  • 打開 NDEBUG 宏;

具體的編譯模式取決於具體實現(意思是我說的不一定準確,我只說個大概,具體要根據編譯器而定)

除了這兩種模式,其實還會有其他模式,比如 CMake 中還有 RELWITHDEBINFO 和MINSIZEREL 模式,它們都是不同編譯選項的集合。

回到主題,這兩種模式中,較為重要的就是是否啟用優化。為了有效 debug,DEBUG 模式不會開啟優化(O0 優化級別),而 Release 為了生成程序的 size 較小,執行效率更高,會啟用 O1 或 O2 等優化級別(經 @ddr Fe 提醒,這裡應該是啟用 O2 或 O3 優化的程度)。

以 clang 來說,O2 相比於 O0,舉幾個我熟悉的(實際有很多,也取決於具體實現):

  • 開啟 ipsccp,會積極的認為變數是常量,做常量替換,會積極的刪除不可達分支;
  • 開啟 globalopt,對全局變數做優化,檢查未引用地址的全局變數,並儘可能用常量做替換;
  • 開啟 mem2reg,儘可能的使用寄存器,從而避免沒必要的 load/store 訪存操作;
  • 開啟 instcombine,合併可以合併的操作,比如對一個變數的兩次運算,如果可以合併就合併;
  • 開啟 inline,對函數做內聯;
  • 開啟 tailcallelim,消除尾調用;
  • 開啟 constmerge,合併重複的全局常量,編譯器如果認為不影響結果就會合併;

從高層語言的角度看,我們能常接觸的幾個點:

  • 變數初始化。O0 情況下,編譯器會為未初始化的段填入默認值,但 O2 時,不會填默認值,如果未初始化,變數的內容就是未知數,這可能導致兩者的運行結果不同。包括其他答案中說的變數未賦值,數組越界,都是這個問題。
  • 檢查調試宏,Debug 模式下會啟用 _DEBUG (類似的,和具體實現相關),Release 下會啟用 NDEBUG。如果用到這幾個宏,結果肯定不同。
  • 內存申請時,可能會出現 Debug 申請更多的空間,Debug 有可能會為了內存對齊而多分配一些空間給你(也取決於實現,和運行庫相關)。
  • 不要忽視警告。作為編譯器開發者,我們可能會把本來是 Error 的情況改成 Warning,內心應該是『還是挺想讓程序過,但過了可能會有問題,咋辦呢,給個警告吧』。
  • 鏈接時不要弄錯鏈接庫。

就想到了這些,有空再補充吧,希望對你有幫助。


2021 年 1 月 25 日補充:

謝謝 @ddr Fe 的補充,我後來重新整理了一下這塊的資料,Release 不會綁定具體的編譯級別,它的優化 pass 集合類似於 O2 和 O3 的水平。不過我本人不常用 O3,不算特別了解。

原文有修改。


我曾遇到,C#程序,Debug能編譯能正常運行,Release能編譯卻在運行時出錯。

我遇到的幾次,本質上都是性能對多線程代碼順序的影響。

比較經典的一個:

目的:彈出「讀取中」彈窗,後台線程載入,載入完畢後彈窗自動關閉。

Debug模式運行正常;

Release模式:彈窗無法自動關閉。

我的實現方式是:事件線程通知界麵線程打開彈窗,(界麵線程打開彈窗並監聽,)事件線程啟動工作線程進行載入,工作線程通知界麵線程(的監聽)關閉彈窗。

括弧內為第三方實現。

經過分析,問題的關鍵在於,界麵線程打開彈窗並啟動監聽這一操作,需要數百毫秒來完成,而在此期間,關閉彈窗的通知會被忽略。

在Debug模式下,最快的載入也要數百毫秒,所以問題不大;但Release模式,最快不到100毫秒即可載入完,並發出關閉彈窗通知,而此時彈窗尚未打開,關閉彈窗的通知被忽略,稍後彈窗打開後,卻不會再有關閉的通知了。

我們的解決方案算是永久的臨時方案……工作線程載入完畢後休眠500毫秒再關閉彈窗。因為我們的程序只運行在特定設備上,性能恆定,所以這個方案就夠了。


籠統地問,籠統地答。

Debug/Release 配置構建,影響到程序行為差異的,一般在兩方面:

(一) 源碼中的條件編譯,及類似的由構建管理器 (makefile, CMake, VS project configuration, etc) 產生的有區別的構建(不同的編譯、鏈接選項,及依賴庫)。

例如,VC 工程的 Debug 版,會定義 _DEBUG 宏,激活由該宏控制的 CRT 調試功能,如向調試器列印文本 _RPT()、斷言檢查 _ASSERT() 等。而在 Release 版,這些調試功能,會被定為空或 ((void) 0),即不在 Release 版編譯時產生代碼。這時,假如你在 _ASSERT(expr) 中的 expr,是一個起實際作用的語句,那麼它就只在 Debug 版里存在,在 Release 版里根本沒有這句 expr。這是一個導致 Debug/Release 版行為不同的初級陷阱,但是確有人犯,因為不光有 CRT 提供的調試功能,有時我們自己也會依據 _DEBUG 寫一些仿照的調試功能,不小心會搞錯。最佳實踐:專用於調試的功能,包括 assert,其中的求值語句,一定不要是正常執行流中起作用的語句,或者用日誌的方式代替調試列印(用日誌級別控制輸出的詳細程度),以及給自定義的調試功能起個明顯的名字。

Debug 構建會鏈接 MSVCRT Debug 版,動態鏈接就是文件名中有字母 d 的 msvcr&d.dll。Release 版鏈接的是無字母 d 的,部署時的 VC Redist Package 安裝包里的是配合 Release 版使用的。

Debug 版還會開啟 RTC 檢查 (Runtime Check)。RTC 是 VC 向未初始化、邊界等關鍵內存區域填充特徵數據,加上調用 MSVCRT Debug 版里特有的 RTC 函數完成的,目的是發現潛藏的運行時錯誤或缺陷,如變數未初始化便使用等。你要是自己手工調用 RTC 功能,那只有 Debug 配置才行。

(二) 由編譯器在 Release 版時的激進優化帶來的與 Debug 版行為不一致。這是 (一) 中分化出來的一類,但適合集中討論。

Debug 版一般關閉編譯優化,編譯器老實地翻譯你的源碼,基本上產生的目標序列,就是你的編寫意圖,不會加入自己的「聰明思考」。

在調試上,這產生 Debug 版相較於 Release 版的特點——更易於做 source-level 調試,斷點、step in/over,很規整地和 C/C++ 源碼行對應。Release 版則不是,無論是否寫 inline,都可能展開到調用處,不可達語句被刪除,很多時候 Release 版調試給源碼某行打斷點,調試時卻發現斷點飄到一邊兒去了。當然,掌握一定彙編、反彙編調試技巧,及可能的編譯優化後,是能勝任調試 Release 版找 bug 的,有時這是唯一要做的事,這個讀那本《軟體調試》[1]或有幫助。

影響調試是小,有時編譯優化會產生與程序員預期相悖的目標序列。例如,編譯期指令重排、循環不變代碼的優化,都可能影響多線程程序的 Debug/Release 版行為不同。

參考

  1. ^《軟體調試》(2008.6) 張銀奎 著。已有更新版。 https://book.douban.com/subject/3088353/


你這種情況需要檢查下數組寫入時是不是越界了。

Debug和Release最簡單的區別,就是在優化層面上,Debug會寫入一堆調試信息,棧和堆的初始化數據也比較獨特,在Debug編譯模式下,你數組越界寫入,覆蓋某些數據,只要運氣夠好沒有改變返回地址等一系列重要數據,是不至於崩的。

但Release不一樣,首先,多餘的東西絕不給你留著,盡量讓程序減小體積。其次,是還存在其他優化,例如指令重排、流水線優化等,二進位數據的直觀順序相比於Debug會有變化。

所以Release相比於Debug,當然更容易崩。


原理都是一樣,沒有什麼本質上的區別。你運行不了的原因可能是因為對應的dll沒有拷貝,例如你debug依賴的動態庫和release是不一樣的,部署的時候得要部署正確。


推薦閱讀:

關於C語言, GCC/MSVC中,如何寫出一個真正意義上的不依賴庫的程序?

TAG:MicrosoftVisualStudio2019 | CC | GCC | 編譯 | 軟體調試 |