在工程實踐中,算法實現常伴隨着處理器選型以及代碼優化兩方面的工作。本文將從算法設計本身和基於特定處理器平台的算法實現這兩個角度,列舉出幾個關鍵的評估維度。通過這些維度的衡量,我們可以一探處理器運算性能的極限,並做出更加優化的算法實現策略。
算法復雜度
算法復雜度是為了在理念層面上比較兩種算法而設計的,僅依據算法本身的內容來比較算法。人們希望借助於算法復雜度分析,來了解如果給算法一個不同的輸入,它將如何表現。
考量一個算法的表現,我們可以把注意力放在度量算法性能的上限上來,也即算法如何面對最壞的情況?它何時會遇到具有挑戰性的艱難任務?這一分析方法稱之為“最壞情況分析”;同時,我們希望在進行算法復雜度分析時,先忽略特定的編程語言和編譯器之間的差異,將分析重點集中在算法本身的思想上。另外,在趨於極限情況時,算法的表現將主要受高階因素項的影響,關注這一點便可以簡化問題的分析。這一分析方法稱之為“漸進行為分析”。
以上兩點,要求我們做算法分析時要保持大局觀,其基本思路是:
-
關注運行時間的增長趨勢。
-
忽略那些依賴於機器的常量和低階因子。
算法復雜度常從兩個維度來評估:時間復雜度和空間復雜度。我們使用時間復雜度來度量算法運行的性能,使用空間復雜度來度量算法要使用的存儲器空間大小。
時間復雜度
時間復雜度用O(f(n))表示,函數f(n)描述了算法計算量隨着計算規模n的變化而變化的趨勢,當n趨於無窮大時,通過f(n)我們就能知道算法在極端(最壞)情況下的計算(時間)消耗情況。
與時間復雜度關聯緊密的一個量是時間頻度T(n),它用於描述一個算法中的語句執行次數。我們可以認為,對時間頻度T(n)做漸進行為表示,得到的就是時間復雜度O(f(n))。因此,在求算法的時間復雜度時,通常是先計算得到算法的T(n)表達式,再進一步求得算法的O(f(n))表示。
以如下的矩陣與向量乘法為例:
for(i=0; i<row_len; i++);
{
temp = 0;
for(j=0; j<col_len; j++)
{
temp += matrix[i][j] * vecter[j];
}
product[i] = temp;
}
設row_len = col_len = n,並不考慮循環跳轉所需的指令時間。在內層循環中,包含一次乘法,一次加法,兩次讀操作和一次寫操作;在外層循環中,包含兩次寫操作。因此該算法的時間頻度可表示為T(n) = 5n^2 + 2n。考慮到時間復雜度忽略低階和常數項的影響,該算法的時間復雜度為O(n^2)。
需要注意的一點是,對於T(n)僅含一個常數項的算法,它的時間復雜度表示為O(1)。
空間復雜度
空間復雜度是指算法在計算機內執行時所需存儲空間的度量,與時間復雜度的分析思想類似,空間復雜度也用O(f(n))表示。
一個算法在計算機存儲器上所占用的存儲空間,包括存儲算法本身所占用的存儲空間,算法的輸入輸出數據所占用的存儲空間和算法在運行過程中臨時占用的存儲空間這三個方面。空間復雜度不考慮存儲算法占用的空間,同時,由於算法的輸入輸出數據所占用的存儲空間是由要解決的問題決定的,它不隨算法的不同而改變,在分析空間復雜度時一般也不予考慮。因此,我們一般所討論的空間復雜度,是用於衡量算法正常占用內存開銷外的輔助存儲單元規模。
以上面時間復雜度分析所用的例子來說,臨時的存儲空間只有一個temp變量,而matrix、vecter、product三者均為輸入輸出變量,固該算法的空間復雜度為O(1)。
實現復雜度
盡管算法復雜度能夠在抽象層面上衡量算法的性能,但在分析算法在具體處理器平台上的表現時,它卻不是一個很好的度量標准。原因有:
-
時間復雜度忽略低階和常數項的影響,而兩個算法復雜度相同的算法有可能在實現上會表現出成倍的性能差異,這些差異和編程語言、編譯器平台、處理器的指令集、處理器的功能架構等有關;
-
空間復雜度也忽略低階和常數項的影響,且不能表征算法訪問存儲器的次數,更沒有考慮到處理器訪存層次結構的影響;
-
不同算法在其實現時所關注的限制因素不同,僅關注時間復雜度或空間復雜度有時是沒有意義的。比如,從來沒有優秀的開發人員使用時間復雜度來衡量網絡服務器或數據庫服務器的性能;
-
時間復雜度不能很好地度量並行算法,並行算法需要的數據/任務划分、通信等都超出了時間復雜度的考量范圍。因此一旦並行算法的這些特性成為了影響性能的重要因素,時間復雜度分析通常會得出與實際不符的結論。
由於以上分析的這些原因,實際應用中,算法復雜度提高但是實現更快的例子很多。為更好地度量算法在實現時的性能,文獻2的作者劉文志提出了實現復雜度的概念。鑒於某個特定的處理器上算法的實際性能基本上由運行時的計算,仿存和指令決定,他將實現復雜度分三個維度來評估:計算復雜度、訪存復雜度和指令復雜度。
在做實現復雜度分析時,應注意以下幾點:
-
脫離抽象回歸具體,評估實現復雜度時已不能像算法復雜度那樣做漸進行為分析,而應該盡可能地還原處理器實現的細節。
-
對於具體的代碼而言,依據在某種處理器上運行的性能瓶頸不同而采用實現復雜度的不同方面來度量。對於一個計算限制算法,應當使用計算復雜度和指令復雜度。對於一個存儲器限制的算法,應當使用訪存復雜度來分析。
計算復雜度
計算復雜度衡量的是每一個控制流的計算數量,它評估處理器對指定算法的計算能力。在並行算法上應用計算復雜度分析時,如果控制流之間的計算量並不均衡,同時考慮控制流之間最大計算復雜度和最小計算復雜度,它們的比例就可大致估計負載不均衡的程度。
與時間復雜度只考慮指令數量不同的是,計算復雜度還需要考慮指令的吞吐量或延遲,將它們加權平均。計算復雜度權重可基於延遲或吞吐量確定,但實際都可轉化為吞吐量。
通常可以把處理器分為為延遲優化設計和為吞吐量優化設計兩類。比如ARM cortex-r系列處理器,它比較注重對事件的響應速度,因此單條指令的延遲是很短的,我們可以把它歸為基於延遲設計的處理器。又比如NVIDIA的GPU,它擁有眾多的並行處理核心,單次可以做多個並行計算,但它每次計算的延遲相對又是比較長的,我們可以把它歸為基於吞吐量優化的處理器。但有時這兩種歸類方法也會變得模糊,因為越來越多延遲性很好的處理器也加入了指令級並行能力。總之,究竟是以延遲還是吞吐量來考量計算復雜度,應該結合處理器的特點以及算法的需求來共同決定,選擇最貼近於現實目的那一個。
例如我們要實現下面的計算:
int a, b, c;
c = a + b;
其計算復雜度公式為O(2n*α + n*β + n*γ),其中α表示加載指令的吞吐量倒數,β表示加法指令的吞吐量倒數,γ表示存儲指令的吞吐量倒數。如果代碼運行在為延遲優化的處理器上,只需將α、β、γ替換成時鍾周期數即可。
以Intel Haswell架構為例,假設數據均在一級緩存中,則α=3,β=0.25,γ=3,固計算復雜度為O(9n + 0.25n),可以看出計算復雜度主要由訪存帶來,這意味着這個算法的瓶頸是訪存。
指令復雜度
指令復雜度衡量執行代碼關鍵路徑所需要的時鍾周期數。它與計算復雜度的區別是:計算復雜度沒有考慮處理器能並行執行多個不同指令的情況,而指令復雜度考慮;計算復雜度適合用於同一處理器平台的不同算法或不同控制流之間的比較,而指令復雜度適合於同一算法的不同實現策略間的比較,因此常用於指導實現性能優化。
執行代碼關鍵路徑的確定方法在信號君之前的文章 《TI C6000 優化進階:循環最重要!》中有過介紹,這里不做贅述。另外,個人認為文獻2中提出的計算復雜度和指令復雜度的概念,可以對應於這篇文章中講到的未分配資源限和已分配資源限,只是表述有所不同。
借文獻2作者劉文志的話強調:代碼計算效率優化的過程,就是指令復雜度降低的過程!
訪存復雜度
訪存復雜度衡量算法訪問緩存層次的字節數量。如在《計算機系統中與存儲有關的那些事》中介紹的,計算機系統中的存儲空間往往是一個多層次結構,訪存復雜度並不是對算法在各層中的訪問性能做加和,而是由瓶頸所在的存儲層次決定。如果算法是一級緩存密集型,則需要分析一級緩存訪問字節數量;如果算法是多核心共享緩存密集型,則需要分析所有處理器核心訪問共享緩存的總數量。
訪存復雜度通常使用緩存層次的帶寬來表示。計算訪存復雜度時,由於緩存缺失(cache miss)帶來的額外開銷也應該計算在內。
有四種與存儲有關的帶寬值得介紹下:
-
存儲器的峰值帶寬:由硬件規格決定,通常可由硬件參數計算得到。如雙路雙通道DDR3-2333內存,其峰值帶寬為2(雙路)*2(雙通道)*2.333*8(64位內存控制器) = 74.6GB/s。
-
可獲得的存儲器帶寬:由於硬件設計方面可能存在不足,可獲得的存儲器帶寬通常要小於存儲器的峰值帶寬,其大小一般由程序測試獲得。
-
程序發揮的存儲器帶寬:某個特定程序在硬件上發揮的帶寬,該值可以通過硬件計數器獲得,頂級軟件開發人員也可手工計算出來。
-
程序的有效訪存帶寬:表示程序最小要訪問的數據量與程序計算時間的比值,可由算法計算獲得。該值能表示訪存優化能達到的上限。
其中,訪存復雜度分析的是程序發揮的存儲器帶寬。程序發揮的存儲器帶寬與可獲得的存儲器帶寬比例,表示程序已經發揮了硬件能夠提供的帶寬的比例。程序有效訪存帶寬和程序發揮的存儲器帶寬比例,表示程序訪問存儲器的效率。
探求計算性能的極限
針對固定的算法和處理平台,性能優化工作往往存在如下圖所示的規律:
當性能優化到一定程度,再想往上提升就需要花更多的時間做人為的指令調度和程序結構調整的嘗試。但不管怎么優化,性能優化一定存在如圖中所示的一道紅線,它是計算性能的極限,是性能優化不可能逾越的終點。
同時,從不同維度如指令、訪存等來看,各維度又分別存在性能優化的天花板。比較容易能想到的是,性能優化的紅線一定比所有維度的天花板都低。
追求極致的性能優化工作都應該試圖去接近紅線。在實踐中就是預先評估算法瓶頸所在,從這一維度出發找到性能優化的天花板,然后不斷試圖接近它。你也許可以使用計算復雜度來確定並行計算中負載均衡的性能天花板;也許可以使用指令復雜度來確定計算效率的天花板;也許還可以使用訪存復雜度來確定訪存優化的天花板……當你撞上了天花板,那么恭喜你,這也是紅線!
比較的科學
從學術研究到工程實踐,從工作到生活休閑,比較無處不在。日常生活中,當我們做出選此而非彼的決定時,大部分時候都是順着感性的引導,也就是我們各自的偏好。那么,當我們需要做出一個嚴謹、理性、可量化的選擇時,我們又該如何科學地去評估面前的選項呢?
在寫完這篇對算法的評估內容后,我想到答案也許就是這三條:
-
選取一個評估維度;
-
如果可以,找到該維度下的極限;
-
如果有必要綜合多個維度做比較,計算維度的加權和。
首先,同一個維度的比較,其結果才是有意義的。代數中“基”、參數估計中的優化准則等,其本質就是一個比較的維度。而如果能在特定維度下確立一個極限,我們就能清晰地知道當前選項所處的位置。就像通信中有“香農限”,參數估計中有“克拉美羅界”。至於多個維度的加權和,那就需要你我來調參咯。
參考資料
【1】翻譯 | 淺析算法復雜度分析--信號君.
【2】劉文志. 並行算法設計與性能優化[M].北京:機械工業出版社,2015.
【3】米歇爾.杜波依斯等著, 范東睿等譯. 並行計算機組成與設計[M].北京:機械工業出版社,2012.
【4】王琤. 計算性能的極限——探求計算密集應用優化的天花板.pdf
【5】Analysis of algorithms--Wikipedia.
·END·
歡迎來我的微信公眾號做客:信號君
專注於信號處理知識、高性能計算、現代處理器&計算機體系
技術成長 | 讀書筆記 | 認知升級
幸會~