用C#實現一個映像邊緣檢測演算法,處理一個300*375的圖片竟然用了2.38s,吐血。用VSTS的程式碼分析工具發現,GDI+裡面的GetPixel()這個函數竟然佔用了已耗用時間的53.92%而SetPixel()佔用了7.14%,光是映像的讀入和輸出就佔了60%+(見圖1)。這樣的速度可受不了。稍微分析一下就能得出原因,Bitmap是一個通用類,可以用來處理多種映像格式,因此GetPixe()l和SetPixel()的效率自然會在諸多分支跳轉中受到拖累,再加上這兩個函數被頻繁反覆調用,對程式效率的影響尤為明顯。再深入想一想,實際上這樣的函數無非是把一塊記憶體複製並返回而已,因此可以考慮直接深入Bitmap的內部用指標糟吊搞,這樣不僅可以省去反覆調用函數壓棧彈棧記憶體配置等瑣碎工作,也能夠在映像格式已經確定下來的前提下節約分支跳轉的時間。
圖1 VSTS中對代碼啟動並執行採樣分析結果
放狗搜了一下,Google大神告訴我們,其實微軟已經考慮到了這樣的效率問題,提供了兩個東西,一個叫做BitmapData類,專門給需要效率的同學實現上面的思路,一個是傳說中的Unsafe Code支援,使得C#中仍然可以使用指標,為BitmapData的使用創造了條件。至於細節上如何使用這個類,執行緒安全性,記憶體如何對齊等問題,詳詢10086,此處不述。或者也可以拜讀這位同學的文章:使用C#進行影像處理的幾種方法。(此文贊一個,寫得相當清楚)
採用這種方案對實現進行改進後,對同一張圖片的處理時間變為了0.19s,時間縮短了92%,這還是可以接受的。當然,進一步的改進仍然需要,這就是另一個問題了。
由此可見,當效率不理想的時候,不要怨天尤人,不要怪C#或Java此類語言先天不足。而應該先用工具搞清楚問題究竟出在什麼地方,然後抓主要矛盾,著重最佳化這幾個函數。此外,有的放狗也是重要技巧之一。一開始用關鍵字"GDI+ 效率 C#",搜出來一堆關於"GDI+比OpenGL效率還高"的資料,後來換了"GetPixel C# 效率",就搜出了相關文章。此外在沒有網路的時候也可以去看看MSDN,事實上如果認真閱讀MSDN中Bitmap.Members這個文檔的話,會發現LockBits()這個函數,深入探究下去也可以找到相當詳細的資料和常式。總而言之,積極的態度,理性的分析和工具的合理利用是克服困難的有力武器。