C#非我所長,但是這不妨我通過一些底層工具來分析C#的異常處理的效能。
在這裡我無意討論異常好或者壞,也無意討論什麼時候該用異常或者不該用。也不深入討論C#和Windows的異常處理機制。我只是給出實驗資料,告訴你異常到底是慢還是快。
兩個實驗都是用同樣一次編譯出來的exe檔案。兩個實驗都是分別使用命令列參數來控制是否拋出異常,然後收集資料來比較。至於不使用try catch和使用try catch但是不拋出異常的情況,我懶得寫了。當然我的實驗並無意證明處理異常的開銷和自己寫的FuncToKillTime的開銷有什麼樣的關係,我只是要說,處理異常很慢。
實驗代碼如下,就是一個簡單的console程式,編譯環境是WinXP SP3+ VS2008 SP1,預設的release配置。
代碼
namespace VtuneExampleCSharp
{
class Program
{
static Int64 BigNumber;
static void FuncToKillTime()
{
for (int i = 0; i < 1000; ++i)
{
BigNumber += i * i;
}
}
static void Main(string[] args)
{
BigNumber = 0;
for (int i = 0; i < 100000; ++i)
{
try
{
FuncToKillTime();
if (args[0] == "throw")
{
throw new Exception("An Exception");
}
}
catch (Exception err){ }
}
System.Console.Write(BigNumber);
}
}
}
實驗1是在我的心愛的Win7的迷你筆記型電腦上做的,它的Atom CPU有超執行緒,所以從作業系統角度來看,這是一個雙核的CPU.
實驗使用Xperf工具觀察拋出異常與否的區別。Xperf是Vista以後微軟推出的效能調試工具,運行時對系統的影響相當的小.
圖一非常直白的顯示了當拋出異常的時候,程式的執行時間大大的增加了。
這張圖分為上下兩個部分,上部分是CPU的使用率,兩條曲線是因為雙核,下部分是程式的生存周期,上下兩部分的X軸都是時間。可以看出,在拋出異常(用藍色的Throw標示的)時,執行時間大約在12秒,而沒有拋出異常時(藍色的No Throw標示的)執行時間大約只有2秒。
圖二是使用Xperf來具體觀察哪些模組(Module)在消耗CPU資源。比如說左邊Throw的情況(程式從5秒執行到17秒),我隨便的選擇了8.6~14.5秒這段時間的來觀察CPU使用率.
那麼我們可以看到,VtuneExampleCSharp.exe分別佔用了49.97%(Throw的情況)和44.13%(No Throw的情況)的CPU資源,其他的都大部分被Idle進程佔用了,這是因為雙核的原因,單線程程式,不可以達到100%
進一步在Xperf中展開VtuneExampleCSharp.exe,於是就可以觀察具體每個模組所佔用的CPU資源,他們的加起來就是整個程式所佔用的CPU資源了。
這裡就是我要展示的結果,各個模組在是否拋出異常時所佔用整個程式資源的比例是大大的不同。
1。比如說左邊No Throw的情況,一個叫Unknown的module佔用了絕大部分的CPU資源。這個叫Unknown的module基本上就等同於main函數的那些代碼,包括FuncToKillTime和Throw的操作,但是不包括Throw以後,系統處理這個異常的操作。至於叫Unknown的原因是因為Xperf暫時還無法解析managed code的symbols檔案,如果實驗是用C++做的話,是可以看到具體的模組名,甚至裡面的函數的情況。
2。值得關注的兩個module是ntkrnlpa.exe和mscorwks.dll, 在No throw的情況下,他們倆的開銷基本上為0。而在Throw的情況下,他們兩個開銷之和佔了整個VtuneExampleCSharp.exe開銷的絕大部分。而這兩個模組其實就是throw以後效能大大下降的根源所在。
(注1:我無意解釋ntkrnlpa.exe和mscorwks.dll是什麼,以及為什麼一個exe可以作為另外一個exe的模組出現,這非我所長)
(注2:我選取得時間段並不相等,所以程式開銷的絕對總量不一樣,所以你無法說8.97%的unknown比43.73%的unknown模組執行快)
實驗1的結論就是,如果沒有Throw的話,幾乎所有的CPU都在跑main函數,而出現了Throw的話,絕大部分CPU資源都被其他系統模組佔用去處理異常了。
實驗2回到了我老掉牙的WinXP SP3的開發機上,這台機器上裝有效能調試的神器Vtune.
實驗2使用可執行檔和實驗1是一樣的,也是測試了拋出與不拋出異常的情況。
首先貼上來的是未經處理資料並簡單的解釋一下每一列的意義。
Module: 在這個process進程空間下有哪些不同的模組被執行過。VtuneExampleCSharp.exe.jit就是我在開篇寫的代碼編譯後屬於的模組
Process:屬於哪一個Process,在這裡我只關注了一個進程,所以都是一樣的。
Instructions Retired samples,Instructions Retired%和Instructions Retired events這三列的意思就是這個模組一共執行了多少條CPU指令。雖然執行指令的條數並不能等同於啟動並執行時間,但是依然可以用來反映程式的開銷。
首先是沒有throw的情況,可以看到最大的開銷依然是VtuneExampleCSharp.exe.jit, 和實驗1的結論一樣。
(mfeapfk.sys是我的McAfee在作怪,可以忽略掉)
在有throw的情況下.
1.VtuneExampleCSharp.exe.jit總共執行的指令數並沒有太大的變化,參考Instructions Retried samples和Instructions Retried Events這兩列,這兩列的值是絕的值,直接反映了這個模組所執行過的指令數。可以發現這些值還略小於沒有throw的情況,當然這並不是說有throw的情況這些代碼還執行得少,這隻是Vtune採樣的誤差罷了。
2.雖然VtuneExampleCSharp.exe.jit的總執行指令數沒有太大的變化,可是占整個進程所執行的指令數的比例卻大大的下降了,原因還是因為有其他模組在處理異常。
再附上根據資料得到的更加直觀的圖,表示的是各個模組所執行指令占整個程式運行所執行指令的百分比。
再次提醒,Throw和No Throw的情況下所執行的指令數的絕對值是不一樣的,在實驗1裡,可以看到已耗用時間是不同的,這裡就可以看到具體多少執行被執行是不同的。
是Throw和No Throw情況下,主要模組所執行的指令數絕對值的比較。可以看到ntoskrnl.exe和mscorwks.dll有巨大的增加,而我自己寫的實驗代碼的模組基本上沒有變化。至於ntoskrnl.exe和mscorwks.dll是什麼,為什麼增加這麼多,我這裡無意去詳細解釋,這也不是我所長。
OK,分析就到這裡了,其實我打得字都有點畫蛇添足了,這幾張圖很直觀了。
-------------------------------------------------------------------我是無聊的分割線------------------------------------------------------------------------
後記與廢話:做實驗和得出結論沒花我多久時間,但是處理資料,圖片和寫blog倒是花了很長的的時間。