李洋+張振東
摘 要:隨著嵌入式處理器性能的不斷提高,處理器性能已經(jīng)不是影響彈載計算機系統(tǒng)整體性能的主要因素。系統(tǒng)升級越來越多地注重程序的執(zhí)行效率、編譯器優(yōu)化能力、程序并行設計等方向。本文從一般性的程序運行切入,分析了影響程序執(zhí)行效率的因素、編譯器優(yōu)化的局限性,從程序定義、減少函數(shù)調(diào)用、提高循環(huán)效率、減少不必要內(nèi)存訪問等角度介紹了一些提高程序執(zhí)行效率的程序設計方法。
關鍵詞:彈載計算機;程序優(yōu)化;編譯器;代碼移動
中圖分類號:TP311 文獻標識碼:A 文章編號:1673-5048(2014)05-0037-04
0 引 言
在嵌入式領域,實時性一直是衡量系統(tǒng)性能的一項重要指標,它主要取決于系統(tǒng)硬件性能和軟件設計兩個方面,以往由于硬件核心處理單元性能的限制,致使整個嵌入式系統(tǒng)的運行環(huán)境、適用范圍、代碼體積、系統(tǒng)性能受到了很大的局限。然而隨著超大規(guī)模集成電路的快速發(fā)展,德州儀器、高通、ARM等公司嵌入式處理器產(chǎn)品線的成熟和廣泛應用,核心處理單元已經(jīng)不是影響系統(tǒng)性能的最主要因素,人們開始越來越多地關注程序的執(zhí)行效率、編譯器的優(yōu)化能力、程序并行設計等問題。
作為嵌入式應用的一個典型代表,彈載計算機也面臨著類似問題,美國AIM-120中程空空導彈經(jīng)歷了數(shù)次改進,每次除了硬件性能有部分升級外,還包含大量的算法改進、代碼優(yōu)化等軟件升級。我國空空導彈目前已經(jīng)發(fā)展到第四代,算法改進、代碼優(yōu)化、并行設計也是在進行軟件升級時所采取的有效手段。
1 影響程序運行效率的因素
高效的計算機程序主要取決于兩方面的因素:①針對問題選擇最適宜的算法和數(shù)據(jù)結構。一個算法的評價主要從時間復雜度和空間復雜度來考慮,對于彈載計算機寶貴的硬件資源和嚴格的強實時性要求而言,選擇使用的算法必須在保證實時性的前提下盡可能少地占用彈載計算機資源。數(shù)據(jù)結構則是對算法的支撐,程序涉及的數(shù)據(jù)都屬于某種數(shù)據(jù)類型,類型明顯或隱含地規(guī)定了數(shù)據(jù)的取值范圍、存儲方式以及允許進行的運算,選擇與算法相適宜的數(shù)據(jù)結構可以帶來更高的運行或存儲效率;②有足夠智能化的編譯器,能夠充分理解程序源代碼的意圖,進而把源代碼轉(zhuǎn)化為高效的可執(zhí)行代碼,同時能夠避免由于優(yōu)化而帶來的諸多問題。然而,就編譯器本身而言具有多方面的局限性。
2 編譯器的局限性
編譯器利用極為復雜的算法來確定程序中的變量是如何參與運算以及被引用的,因此編譯器可以盡量簡化源程序的計算方法,比如用移位操作代替源程序中的乘法運算。然而,編譯器對程序進行優(yōu)化時仍受到三個方面的限制:①不能改變源程序的計算結果;②編譯器的算法通常不能完全理解源程序的思想,以及變量使用的環(huán)境;③整個編譯過程的時間是可接受的。
2.1 存儲器別名使用
voidaliase2(intpt1,intpt2){
pt1+=2pt2;}
兩段程序似乎具有相同的功能,它們都是把指針pt2指向變量值的2倍加到指針pt1指向的變量上,但是由于aliase2進行了3次訪問內(nèi)存的操作,而aliase1卻訪問了6次。因此,編譯器在對aliase1進行優(yōu)化時如果按照aliase2那樣生成代碼,程序?qū)碛懈叩膱?zhí)行效率。然而,如果當pt1和pt2同時指向同一內(nèi)存單元時,那么程序aliase1將執(zhí)行如下操作:
pt1+=pt1;pt1+=pt1;
執(zhí)行后的結果是pt1指向內(nèi)存單元的內(nèi)容是執(zhí)行前的4倍,而aliase2則進行另一種操作:
pt1+=2pt1;
很顯然,執(zhí)行后的結果是pt1指向內(nèi)存單元的內(nèi)容是執(zhí)行前的3倍。實際中,編譯器不會知道aliase1是如何被調(diào)用的、兩個參數(shù)的關系是什么,因此編譯器必須假設pt1和pt2是可能相等的,從而它不能按照aliase2那樣生成代碼。
上述這種現(xiàn)象稱為存儲器別名使用,編譯器必須假設程序中的不同指針在初始化后或在運行過程中是可能指向同一地址單元的,這種假設極大地限制了編譯器對程序的優(yōu)化能力。
2.2 函數(shù)調(diào)用的副作用
限制編譯器優(yōu)化能力的另一個因素來自于函數(shù)調(diào)用,先看如下兩個函數(shù):
intfun(int);intfun1(x){
returnf(x)+f(x)+f(x)+f(x);}
兩個函數(shù)看來似乎有相同的計算結果,fun2調(diào)用fun一次,而fun1調(diào)用fun四次,對于函數(shù)的調(diào)用,計算機要進行壓棧等一系列復雜的操作,因此fun1比fun2具有更大的開銷,在運算結果相同的前提下,編譯器應該按照fun2去優(yōu)化代碼,但是函數(shù)fun如果是如下的結構:
intcounter=0;intfun(intx){
returncounter++;}函數(shù)fun就改變了一個全局變量的值,全局變量的值會隨著函數(shù)fun被調(diào)用的次數(shù)不斷改變。再考慮fun1和fun2,fun1返回的值是0+1+2+3=6,而調(diào)用fun2返回的值是40=0。
這種現(xiàn)象稱為函數(shù)調(diào)用的副作用,編譯器在進行編譯時不會去判斷一個函數(shù)的調(diào)用是否具有副作用,也不會去把原函數(shù)試著替換為另一個擁
2.3 編譯器設計的權衡
除了上述兩種情況,某些編程語言由于自身的特性,往往比其他編程語言難于優(yōu)化,例如C語言允許指針運算和強制類型轉(zhuǎn)換操作,這些特性會影響編譯器對代碼的理解和優(yōu)化。同時,在進行程序優(yōu)化時,通常還要考慮一些其他因素,比如程序是如何運行,或者在可讀性與高效率之間進行權衡等等。
由此看來,由于編譯器優(yōu)化的局限性,一個足夠智能的編譯器設計起來異常復雜,很可能需要針對目標程序的不同應用而專門設計。編譯器在優(yōu)化目標代碼時很可能會改變原有的程序結構和代碼次序,因此這種編譯器在設計時也需要考慮足夠多的條件以避免優(yōu)化帶來的錯誤,需要耗費人們大量的時間和精力,且不能通用。從理論上來說由于過于復雜,每次編譯過程也需要相當長的時間,因此,智能編譯器從目前來看是不切實際的想法。
換個角度考慮,由于代碼中細小的改動都可能使編譯器對代碼的優(yōu)化起到很大的幫助,因此除了依賴編譯器對目標程序的優(yōu)化,程序員如果在寫程序時更加注重代碼的效率,也可以提高程序執(zhí)行的效率。
3 高效的程序設計
程序運行時間從理論上講主要取決于它的輸入規(guī)模,在實際中還受具體處理器性能、I/O、方法調(diào)用等條件的影響,當輸入規(guī)模一定時,針對后者對程序代碼做一些優(yōu)化,將提升代碼的執(zhí)行效率。
3.1 程序定義
為了說明代碼優(yōu)化對程序執(zhí)行效率的影響,這里用處理向量數(shù)據(jù)結構的程序段為例。首先定義結構體:
typedefstruct{
intlength;intdata;
}vec_rec,vec_ptr;
如圖1建立一個定長的向量結構,作為程序的輸入規(guī)模。定義工作函數(shù),作為優(yōu)化目標函數(shù):voidtest1(vec_ptrv,intdest){
inti;
for(i=0;i intval; get_vec_element(v,i,&val);dest=(dest)val; }} 其中vec_length(v)用來取得向量v的長度,get_vec_element(v,i,&val)用來把向量v中第i個值賦給val,兩個函數(shù)都用C語言實現(xiàn),這里省略。 在默認環(huán)境下,使用某型處理器,通過編譯執(zhí)行,與使用GCC編譯時“-O2”優(yōu)化選項相較。通過代碼分析軟件得出優(yōu)化前后兩個程序的CPE(cyclesperelement,每元素周期數(shù))分別是41.86和33.25,很明顯,如果執(zhí)行優(yōu)化編譯,可以提升代碼效率。 3.2 消除循環(huán)中的低效率 通過分析test1發(fā)現(xiàn),函數(shù)vec_length會隨著for循環(huán)的進行每次迭代都被調(diào)用,而向量v的長度是一定的,不會隨著每次循環(huán)而改變,因此可以在循環(huán)前只計算一次向量的長度,并把它賦給一個局部變量length,由此得到改進后的程序test2: voidtest2(vec_ptrv,intdest){ inti; intlength=vec_length(v); for(i=0;i intval; get_vec_element(v,i,&val);dest=(dest)val; } } test2的CPE是21.25,這種方法也叫代碼移動,對于代碼中一些每次計算都得到相同值的部分,可以把它提前到程序的某一行,避免重復計算帶來的開銷。鑒于之前提到的編譯器局限性,在進行代碼移動時,編譯器不能檢測被移動的部分是否具有函數(shù)調(diào)用的副作用,目前最復雜的編譯器依然無法對所有程序做出這種檢測,因此在這種情況下只有通過程序員自己的判斷來進行優(yōu)化。這個例子很好地證明了在程序中看似簡單的部分通常隱藏著與輸入規(guī)模成正比的低效率。 3.3 減少函數(shù)調(diào)用 函數(shù)調(diào)用時涉及壓棧等一系列操作,隱含著通過上述方法,程序的執(zhí)行效率得到了提高,當然這是在輸入規(guī)模達到一定程度的情況下得到的結果,特別是對某些計算密集型程序而言。然而,在實際中并不是所有程序都擁有這種模型,這就需要其他的專業(yè)知識,比如對所用處理器的特性有深層次的理解,熟練掌握對應的匯編語言,從而對編譯器如何生成、執(zhí)行代碼有充分的認識,在進行程序優(yōu)化時才能更好地去實現(xiàn)最初的預想。
換個角度考慮,由于代碼中細小的改動都可能使編譯器對代碼的優(yōu)化起到很大的幫助,因此除了依賴編譯器對目標程序的優(yōu)化,程序員如果在寫程序時更加注重代碼的效率,也可以提高程序執(zhí)行的效率。
3 高效的程序設計
程序運行時間從理論上講主要取決于它的輸入規(guī)模,在實際中還受具體處理器性能、I/O、方法調(diào)用等條件的影響,當輸入規(guī)模一定時,針對后者對程序代碼做一些優(yōu)化,將提升代碼的執(zhí)行效率。
3.1 程序定義
為了說明代碼優(yōu)化對程序執(zhí)行效率的影響,這里用處理向量數(shù)據(jù)結構的程序段為例。首先定義結構體:
typedefstruct{
intlength;intdata;
}vec_rec,vec_ptr;
如圖1建立一個定長的向量結構,作為程序的輸入規(guī)模。定義工作函數(shù),作為優(yōu)化目標函數(shù):voidtest1(vec_ptrv,intdest){
inti;
for(i=0;i intval; get_vec_element(v,i,&val);dest=(dest)val; }} 其中vec_length(v)用來取得向量v的長度,get_vec_element(v,i,&val)用來把向量v中第i個值賦給val,兩個函數(shù)都用C語言實現(xiàn),這里省略。 在默認環(huán)境下,使用某型處理器,通過編譯執(zhí)行,與使用GCC編譯時“-O2”優(yōu)化選項相較。通過代碼分析軟件得出優(yōu)化前后兩個程序的CPE(cyclesperelement,每元素周期數(shù))分別是41.86和33.25,很明顯,如果執(zhí)行優(yōu)化編譯,可以提升代碼效率。 3.2 消除循環(huán)中的低效率 通過分析test1發(fā)現(xiàn),函數(shù)vec_length會隨著for循環(huán)的進行每次迭代都被調(diào)用,而向量v的長度是一定的,不會隨著每次循環(huán)而改變,因此可以在循環(huán)前只計算一次向量的長度,并把它賦給一個局部變量length,由此得到改進后的程序test2: voidtest2(vec_ptrv,intdest){ inti; intlength=vec_length(v); for(i=0;i intval; get_vec_element(v,i,&val);dest=(dest)val; } } test2的CPE是21.25,這種方法也叫代碼移動,對于代碼中一些每次計算都得到相同值的部分,可以把它提前到程序的某一行,避免重復計算帶來的開銷。鑒于之前提到的編譯器局限性,在進行代碼移動時,編譯器不能檢測被移動的部分是否具有函數(shù)調(diào)用的副作用,目前最復雜的編譯器依然無法對所有程序做出這種檢測,因此在這種情況下只有通過程序員自己的判斷來進行優(yōu)化。這個例子很好地證明了在程序中看似簡單的部分通常隱藏著與輸入規(guī)模成正比的低效率。 3.3 減少函數(shù)調(diào)用 函數(shù)調(diào)用時涉及壓棧等一系列操作,隱含著通過上述方法,程序的執(zhí)行效率得到了提高,當然這是在輸入規(guī)模達到一定程度的情況下得到的結果,特別是對某些計算密集型程序而言。然而,在實際中并不是所有程序都擁有這種模型,這就需要其他的專業(yè)知識,比如對所用處理器的特性有深層次的理解,熟練掌握對應的匯編語言,從而對編譯器如何生成、執(zhí)行代碼有充分的認識,在進行程序優(yōu)化時才能更好地去實現(xiàn)最初的預想。
換個角度考慮,由于代碼中細小的改動都可能使編譯器對代碼的優(yōu)化起到很大的幫助,因此除了依賴編譯器對目標程序的優(yōu)化,程序員如果在寫程序時更加注重代碼的效率,也可以提高程序執(zhí)行的效率。
3 高效的程序設計
程序運行時間從理論上講主要取決于它的輸入規(guī)模,在實際中還受具體處理器性能、I/O、方法調(diào)用等條件的影響,當輸入規(guī)模一定時,針對后者對程序代碼做一些優(yōu)化,將提升代碼的執(zhí)行效率。
3.1 程序定義
為了說明代碼優(yōu)化對程序執(zhí)行效率的影響,這里用處理向量數(shù)據(jù)結構的程序段為例。首先定義結構體:
typedefstruct{
intlength;intdata;
}vec_rec,vec_ptr;
如圖1建立一個定長的向量結構,作為程序的輸入規(guī)模。定義工作函數(shù),作為優(yōu)化目標函數(shù):voidtest1(vec_ptrv,intdest){
inti;
for(i=0;i intval; get_vec_element(v,i,&val);dest=(dest)val; }} 其中vec_length(v)用來取得向量v的長度,get_vec_element(v,i,&val)用來把向量v中第i個值賦給val,兩個函數(shù)都用C語言實現(xiàn),這里省略。 在默認環(huán)境下,使用某型處理器,通過編譯執(zhí)行,與使用GCC編譯時“-O2”優(yōu)化選項相較。通過代碼分析軟件得出優(yōu)化前后兩個程序的CPE(cyclesperelement,每元素周期數(shù))分別是41.86和33.25,很明顯,如果執(zhí)行優(yōu)化編譯,可以提升代碼效率。 3.2 消除循環(huán)中的低效率 通過分析test1發(fā)現(xiàn),函數(shù)vec_length會隨著for循環(huán)的進行每次迭代都被調(diào)用,而向量v的長度是一定的,不會隨著每次循環(huán)而改變,因此可以在循環(huán)前只計算一次向量的長度,并把它賦給一個局部變量length,由此得到改進后的程序test2: voidtest2(vec_ptrv,intdest){ inti; intlength=vec_length(v); for(i=0;i intval; get_vec_element(v,i,&val);dest=(dest)val; } } test2的CPE是21.25,這種方法也叫代碼移動,對于代碼中一些每次計算都得到相同值的部分,可以把它提前到程序的某一行,避免重復計算帶來的開銷。鑒于之前提到的編譯器局限性,在進行代碼移動時,編譯器不能檢測被移動的部分是否具有函數(shù)調(diào)用的副作用,目前最復雜的編譯器依然無法對所有程序做出這種檢測,因此在這種情況下只有通過程序員自己的判斷來進行優(yōu)化。這個例子很好地證明了在程序中看似簡單的部分通常隱藏著與輸入規(guī)模成正比的低效率。 3.3 減少函數(shù)調(diào)用 函數(shù)調(diào)用時涉及壓棧等一系列操作,隱含著通過上述方法,程序的執(zhí)行效率得到了提高,當然這是在輸入規(guī)模達到一定程度的情況下得到的結果,特別是對某些計算密集型程序而言。然而,在實際中并不是所有程序都擁有這種模型,這就需要其他的專業(yè)知識,比如對所用處理器的特性有深層次的理解,熟練掌握對應的匯編語言,從而對編譯器如何生成、執(zhí)行代碼有充分的認識,在進行程序優(yōu)化時才能更好地去實現(xiàn)最初的預想。