張合花,張全法,李素曉
(鄭州大學 物理工程學院,鄭州 450001)
C語言程序設計和C++語言程序設計是許多高校開設的計算機語言課程,C/C++也是廣大工程技術人員最常用的程序設計語言之一[1]。C/C++程序可以獲得很高的運行速度,但是并非所有的C/C++程序一經寫出就具有了無可超越的運行速度,往往還需要進行優化。優化一般包括提高運行速度和縮小可執行文件大小兩方面的含義,本文提及的優化是指前者。
優化可以分為自動優化和人工優化兩種方式。自動優化是指利用C/C++編譯器的優化選項進行優化。一般的C/C++編譯器支持靜態自動優化,采用LLVM(Low Level Virtual Machine)編譯架構時還可以支持動態自動優化[2]。人工優化是指程序員利用經驗和技巧進行優化,常用方法包括采用自增自減運算、利用指針處理數組、合理使用內聯函數、采用位運算、合理使用寄存器變量以及盡量將浮點數運算轉化為整數運算等[3-7]。本文主要討論如何利用內存拷貝函數對C/C++程序進行人工優化。
內存拷貝函數memcpy()是C運行庫中的函數,在C++中仍然可以使用,使用時一般需要包含頭文件memory.h。在VC 6.0中其函數原型為
void * memcpy(void * dest,const void * src,
size_t count),
其功能是將以src為數據源首地址的內存中的數據拷貝到以dest為目的地首地址的內存中,要拷貝的數據量為count字節。由于采用了快速內存拷貝技術,其運行速度比循環賦值快得多(參見后面的實驗結果)。因此,在需要拷貝大量數據時,利用它可以顯著提高程序的運行速度。
在VC 6.0的幫助系統MSDN中僅要求這兩塊內存不相互重疊(否則不能正確地拷貝所有數據),對于3個參數并沒有特殊要求和說明。但是,實踐發現只有當src和dest皆為字的首地址且count為字長的整數倍時,才可以獲得最快的運行速度。實際應用中可能不滿足這些條件,需要采取適當的措施盡量滿足這些條件。
以利用VC 6.0編寫實時圖像處理中經常用到的灰度圖像空間域均值濾波函數為例,說明如何采取措施提高程序的運行速度。
假設濾波前的灰度圖像用g表示,寬M像素,高N像素;濾波后的圖像用b表示;所用模板寬(2K+1)像素,高(2L+1)像素。M、N、K、L皆為整數。空間域均值濾波算法可以描述為
(1)
式中:bij—圖像b上坐標為(i,j)的像素的灰度值;
gmn—圖像g上坐標為(m,n)的像素的灰度值。
對于圖像中部的像素,根據式(1)求bij不存在任何問題。對于圖像邊緣附近的像素,根據式(1)求bij將出現困難,因為此時與模板對應的一些像素不存在。處理此問題的方法有多種,包括對邊緣或角上的像素設計特殊模板、假設圖像是環繞的、不處理邊緣像素等[8]。
由于涉及大量的運算,在那些還需要實時進行其他復雜處理的應用中,必須采取措施來提高其運行速度。最簡單的方法是采用K=1,L=1的最小模板來減少計算量。從另一角度來看,由于模板越大圖像信息丟失越嚴重,K和L也不宜太大。此外,不處理邊緣像素也有助于減少計算量。
文獻[9]介紹的盒濾波方法利用上、下或左、右兩個模板位置之間的相關性來減少重復計算,從而提高其運行速度。它利用一個含M個元素的數組B來存儲圖像g上以第j行為中心的(2L+1)行相應列像素灰度值之和。計算圖像b的第一行時通過求和初始化B,計算完一行之后通過加上新的一行、減去舊的一行更新B。求bij時先計算B中以第i個元素為中心的(2K+1)個元素的和S,再將S除以(2K+1)與(2L+1)的乘積。每一行的第一個S通過求和初始化,模板向右移動時,S通過加上新的元素、減去舊的元素來更新。
對于相同大小的模板,從減少重復計算的角度看,盒濾波已經做到了極限[6]。文獻[6]通過實驗證明其運行速度可以進一步提高,采取的措施包括:通過指針訪問內存中的數據,并利用自增運算移動指針;在K=1、L=1時利用3個臨時變量取代數組B來保存中間結果,從而將內存訪問轉化為寄存器訪問;將除法運算轉化為移位運算;等等。
如果不需要將濾波后的圖像寫入原始圖像的數據存儲區,文獻[6]所獲得的速度已經沒有多大提升空間(除非使用更快的硬件),反之,還可以顯著提高,這需要分析此種情況下的數據拷貝操作。為了方便,假設不處理邊緣像素,并且計算機的內存足夠用。不處理邊緣像素并不意味著丟棄邊緣像素(例如賦零),而是保留邊緣像素原來的灰度值,因為丟棄邊緣像素對圖像的影響比較大。內存不夠用時編程將很復雜,但這不屬于本文討論的范疇。
根據式(1)可知,求bij時所用的gmn是原始圖像上像素的灰度值,如果將求出的bij直接寫入原始圖像數據存儲區而不采取措施,將嚴重影響后面的計算。處理這個問題的方法可以分為有備份法和無備份法兩種。有備份法需要預先將原始圖像的數據備份到臨時存儲區,然后根據臨時存儲區中的數據計算bij并直接寫入原始圖像數據存儲區。無備份法不需要備份原始圖像數據,它是根據原始圖像數據計算bij并寫入臨時存儲區,等到計算完成后再將臨時存儲區中的數據寫入原始圖像數據存儲區,不過此時需要分行拷貝,并拷貝到正確位置。
通過以上分析可知,如果需要將濾波后的圖像寫入原始圖像的數據存儲區,無論有備份法還是無備份法皆不可避免大量的數據拷貝操作(數據備份實質上也是數據拷貝)。如果能夠實現數據的快速拷貝則可以進一步提高其運行速度。
一般認為,減少要拷貝的數據量可以提高數據拷貝的速度。在不處理邊緣像素時,有備份法要拷貝的數據量為M×N字節(灰度數據占1個字節),無備份法為(M-2K)×(N-2L)字節。這是因為無備份法不需要拷貝邊緣像素的灰度值,而有備份法則很難避免備份邊緣像素的灰度值,如果采取復雜措施刻意避免,往往得不償失。因此,無備份法要拷貝的數據量比較少,有可能獲得較快的速度。
利用memcpy()函數提高數據的拷貝速度也有助于提高運行速度。有些人不知道C語言的運行庫中有專門用于拷貝內存數據的函數,即使知道該函數,也有一部分人認為使用它未必就好,因為一般常識是函數調用需要額外的開銷,通常會影響運行速度。另外,通過循環賦值實現數據拷貝也很容易。因此,很多人采取循環賦值方式實現數據拷貝。但是,當數據拷貝操作在所有操作中所占比例較大時,memcpy()函數的優勢就顯現出來了。
通過以上分析似乎可以得出memcpy()函數與無備份法相結合是處理數據拷貝問題的最佳組合的結論,其實不然。因為要充分發揮該函數的優勢還需要滿足一些條件,而采用無備份法又不處理邊緣像素時無法同時滿足這些條件。這是因為:每次要拷貝的數據量(M-2K)通常不是字長的整數倍;盡管系統分配的數據存儲區的首地址是字的首地址,由于(M-2K)通常不是字長的整數倍,使得src通常不是字的首地址;為了拷貝到正確位置,dest通常也不是字的首地址;多次拷貝會影響速度。
采用有備份法可以同時滿足這些條件。由系統分配的數據存儲區的首地址是字的首地址,從而保證了src和dest皆是字的首地址;由攝像器件產生的圖像的寬度M是字長的整數倍,從而保證了要拷貝的數據量是字長的整數倍(對于人工隨意裁剪的圖像,存儲時每一行的數據量必須是字長的整數倍才能夠正確顯示)。因此,memcpy()函數與有備份法相結合反而有可能獲得更快的速度,盡管不處理邊緣像素時需要拷貝的數據總量比較多。實驗證明確實如此。
所用計算機的CPU為Intel Pentium Dual E2140處理器,主頻為1.60GHz,程序用VC 6.0編寫。在發行版下利用VC 6.0的Build/Profile功能測試函數的運行時間,自動優化選項為最大速度。用一段彩色視頻作為處理對象,該段視頻共3845幀,每幀圖像的M=384,N=288。
對于每幀圖像,首先采用文獻[5]中的方法進行灰度化,然后調用空間域均值濾波函數對灰度圖像進行濾波。設計空間域均值濾波函數時所用模板的K=1,L=1,并且不處理邊緣像素。為了進行比較,采用不同方法進行設計,并分別進行測試。為了減少Windows多任務特性的影響,每種設計都需要完成5次整段視頻的處理,每完成一次處理記錄一次空間域均值濾波函數的總運行時間,最后計算平均總運行時間。
設計空間域均值濾波函數時涉及數據計算與數據拷貝兩個部分。數據計算部分的功能是求bij,采用文獻[6]中的方法進行快速計算。區別在于:若采用無備份法,gmn來源于原始圖像數據存儲區,bij寫入臨時存儲區;若采用有備份法,gmn來源于臨時存儲區,bij直接寫入原始圖像數據存儲區。數據拷貝部分采用不同的實現方法,具體如下:
設計1 采用無備份法,數據拷貝通過分行循環賦值(故意這樣)實現。
設計2 采用無備份法,數據拷貝通過多次(必須這樣)調用memcpy()函數實現。
設計3 采用有備份法,數據拷貝通過多次(故意這樣)調用memcpy()函數實現。
設計4 采用有備份法,數據拷貝通過一次(可以這樣)調用memcpy()函數實現。
為了不失公允,所有設計都盡可能多地采取措施提高運行速度。所采取的措施包括賦值縮寫、指針訪問、指針自增運算和指針比較等。指針比較是指通過比較指針變量是否到達結束位置作為是否結束循環的條件,從而避免使用額外的循環變量。
實驗結果如表1所示。根據設計2和設計1的平均總運行時間可知,利用memcpy()函數拷貝數據比通過循環賦值快得多。根據設計3和設計2的平均總運行時間可知,當滿足所需條件時可以充分發揮memcpy()函數的優勢,盡管設計3比設計2每行需要多拷貝2個字節,并且每幀圖像需要多拷貝2行。根據設計4和設計3的平均總運行時間可知,整體拷貝比分行拷貝快。可以求出,設計4處理一幀圖像所用的平均時間為0.668 ms,運行速度比設計1提高了8.3%。

表1 不同設計的總運行時間 ms
根據上述實驗數據,設計4比設計1的運行速度提高得似乎很有限,但是上述總運行時間中還包含了數據計算部分所需要的時間,如果將數據計算部分注釋掉,再按照上述方法進行測試,求得設計1~設計4數據拷貝部分的平均總運行時間分別為375ms,224ms,188ms和142 ms,設計2~設計4比設計1的數據拷貝部分運行速度分別提高了40 %,50 %和62 %。這充分說明了使用memcpy()函數取代循環賦值的必要性,也說明了正確使用該函數的重要性。
本文討論了利用memcpy()函數對C/C++程序進行人工優化的問題。以設計圖像處理中常用的空間域均值濾波函數為例,討論了如何通過分析尋找進一步提高程序運行速度的途徑,并通過實驗證明利用本文所述方法可以顯著提高程序的運行速度。這種優化程序的思想方法可以為工程技術人員和有關人員設計需要快速處理大量數據的C/C++程序提供參考。
[1]張合花,張全法,齊永奇.準確使用C/C++中等于運算符的研究[J].中州大學學報,2016,33(2):112-115.
[2]朱曉珺,李冬梅.C/C++程序的運行時優化研究[J].軟件導刊,2009,8(4):60-62.
[3]唐娟.合理提高C/C++程序效率的方法[J].孝感學院學報,2006,26(3):68-70.
[4]張志軍,孫志輝.基于VC平臺的彩色圖像的灰度化技術[J].自動化技術與應用,2005,24(1):61-63.
[5]張全法,楊海彬,任朝棟,等.彩色圖像的快速高保真灰度化方法研究[J].鄭州大學學報(理學版),2011,43(3):66-69.
[6]張全法,費光彥,王慶峰,等.視頻車輛監控系統圖像濾波算法的研究[J].應用光學,2012,33(1):85-89.
[7]張全法,陳倩,王東亞.幀差分智能視頻監控系統圖像亮度的校正[J].應用光學,2013,34(5):778-783.
[8]Russ J C.數字圖像處理[M].6版.余翔宇,等,譯.北京:電子工業出版社,2014:135-140.
[9]舒志龍,阮秋琦.一種二維均值濾波快速算法及其應用[J].北方交通大學學報,2001,25(2):22-24.