瞭解進階處理器功能以提升程式碼撰寫的效能
本文作者:admin
點擊:
2006-01-10 00:00
前言:
今天的數位訊號處理器(DSP)在效能、週邊組合、功耗以及價格方面已達到如此優異的組合,許多系統設計業者都捨棄了傳統設計上使用的處理器,而急切地想要探索新技術的優點。在此有一個可能的障礙,就是他們已針對其應用空間開發了大量的傳統C/C++程式碼。無疑地,這些工程師會想在DSP平台上利用既有的高階程式碼基底(Code Base),並同時利用DSP架構功能以達到無法在舊有平台上達到的效能。此外,他們還需要熟悉且直覺的開發環境,也要能以簡易的方式選擇性地實作組合語言常式以提升效能。本文就現今的開發環境討論DSP的程式策略和技巧。
高階語言與組合語言的比較 - 兩者組合也許是最佳方式
在進行DSP架構的專案時,一定要決定要使用何種的程式設計方法。通常的選擇是組合語言或是C或C++等高階語言(HLL)。這項決定牽涉到許多因素,所以瞭解每種方法所帶來的優點和缺點是很重要的。
C/C++ 的優點包括模組化、可攜性和可重複使用性。不但大多數的嵌入式程式設計人員都使用過這些高階語言,而且這些語言還有龐大的程式碼基底,可以透過相當簡易的方式,從現有的微控制器或DSP領域移植到新的DSP平台。因為每種架構使用的組合語言不同,所以通常相同處理器產品系列中的裝置才能重複使用這些程式碼。此外,在開發小組中常常是由不同的小組負責不同系統模組的程式碼,而HLL可以使這些跨功能小組不受處理器的限制。
長久以來,傳統的組合語言就因為艱澀的語法和奇特的縮寫而倍受韃伐。然而,現在在使用所謂「代數式語法」的架構中,這些因素已不再構成問題。圖1所示的典型DSP指令範例顯示傳統樣式與代數式格式的比較。後者的結構明顯地較為直覺化。
組譯碼向來難以撰寫的原因之一,是它將重點放在 DSP 實際暫存器組、運算單元和記憶體之間的資料流。在C/C++中,這種處理通常會透過變數和函數/程序呼叫的使用,以較為抽象層級的方式進行,使程式碼較易於理解。
今天的C/C++編譯器功能相當完整,將HLL程式碼編譯為嚴謹的組譯程式碼時,許多編譯器的表現都相當傑出。事實上,很多時候讓最佳化編譯器 (Compiler Optimizer)克盡職責是最理想的方式。不過,編譯器的效能仍會針對工具開發人員視為最重要的特定功能集進行調整,因此並非在所有狀況下都可以超越手動建立的組譯程式碼。
最重要的是,程式開發人員只在必要時,才會為了在DSP上有效地執行,使用組合語言來最佳化需要密集處理作業的重要程式碼區塊。HLL編譯器最佳化參數可以有搶眼的表現,但是沒有什麼比得上直接控制DSP資料流和運算的周詳作業。這就是為何程式設計人員經常使用C/C++和組譯碼的組合。HLL適合基本的資料處理和控制,高效率的數值運算則是組譯碼的專長。
高效率程式設計的架構功能
為了讓組譯碼程式設計人員能執行有效的工作,一定要瞭解何種類型的結構可以在DSP和針對超快數字運算而最佳化的處理器之間構成差異。這些功能包括:
* 專用的定址模式
* 硬體迴圈建構
* 可快取的記憶體
* 每週期多重作業
* 連鎖管線
* 彈性的資料暫存器列
這些功能都可能對運算效率造成極大的差異。以下會逐一討論每項功能。
專用的定址模式
處理器若要在單一週期中存取多個資料字組,需要完整的位址產生彈性。除了DSP為中心的存取大多以16和32位元為界限外,要達到最有效的處理,還必須使用位元組定址。這點很重要,因為有些常見的應用(包括許多視訊系統)都是以8位元資料作業。當記憶體存取限於單一界限時,可能需要額外的週期才能讓處理器遮掩相關的位元。
另一項有用的定址功能是「循環式緩衝」(Circular Buffering)。這項功能必須受到處理器的直接支援,而不會造成特殊的軟體管理負荷。透過循環式緩衝,程式設計人員可以定義記憶體中的緩衝區,並自動進行處理。緩衝區一旦設定,就不需要透過軟體進行特殊的互動,即可在資料中巡覽。位址產生器會處理非統一的步距(Stride),更重要的,還會處理圖2所示的「折疊」功能。若沒有這種自動化的位址產生功能,程式設計人員就必須手動追蹤緩衝區,因而浪費寶貴的處理週期。
位元倒置是FFT和DCT之類的高效率訊號處理作業必要的定址模式。如同名稱的暗示,「位元倒置」是在二進位的位址中倒置位元;也就是說,最不重要的位元會與最重要位元互換位置。Radix-2 butterfly所需的資料順序是「位元倒置」順序,所以會使用已倒置位元的索引結合FFT階段。可以在軟體中計算這些已倒置位元的索引,但這種做法非常沒有效率。圖3顯示位元倒置位址流程的範例。
硬體迴圈建構
迴圈是通訊處理演算法的關鍵功能。有兩種主要的迴圈相關功能可提升多種演算法的效能:第一種稱為「零負荷硬體迴圈」(Zero-Overhead Hardware Loop)。與定址功能相同,迴圈建構是以硬體實作。同樣地,這項功能可以在軟體中完成,但相關的負荷就會計入即時處理的預算。透過零負荷的迴圈,程式設計人員可以藉由設定計數值和定義迴圈範圍來初始化迴圈。處理器會持續執行這個迴圈,直到計數達到為止。
零負荷迴圈是多數處理器的一部分,但「硬體迴圈緩衝區」確實可在迴圈建構中提升效能。這些緩衝區可充當迴圈中所執行指令的一種快取類型。例如,在第一次執行完迴圈後,可以將指令保存在迴圈緩衝區內,這樣每次執行迴圈時,就不必一再「重新擷取」同樣的指令。這樣可以節省大量的週期,因為迴圈指令會被保留在緩衝區內,可透過單一週期加以存取。這項功能不需程式設計人員進行額外的設定,但必須知道這個緩衝區的大小,如此才能聰明地選擇迴圈大小。
可快取的記憶體
一般的DSP通常具有少量的快速片上記憶體,微控制器則通常可以存取大型的外部記憶體。階層式的記憶體架構結合了這兩種方式的優點,提供數種具有不同效能等級的記憶體層級。對於需要最高決定性的應用,可以藉由單一的核心時鐘週期存取片上SRAM。對於具備較多程式碼的系統,則可使用大型、延遲度較高的片上及片外記憶體。
這個階層本身並不是特別有用,因為今天的高速處理器會用低得多的速度有效執行,這是因為較大的應用只能放入較慢的外部記憶體。此外,程式設計人員會被強迫以手動方式將重要的程式移出和移入內部SRAM。不過,藉由將資料和指令快取加入至架構,外部記憶體的可管理性會提高許多。快取會減少以手動方式將指令和資料移入處理器核心的機會。因為不必擔心如何管理進入核心的資料和指令流,所以程式設計模型可大幅簡化。
圖4展示一般的記憶體配置,在需要時會由外部記憶體帶入指令。指令快取通常會以某種類型的「近來最少使用」(LRU,Least Recently Used)演算法運作,確保較常執行的指令較少被取代。圖中還顯示,如果可以將某些片上資料記憶體配置為快取而將某些配置為SRAM,可以使效能最佳化。DMA控制器可以直接供應核心所需,而資料表的資料則可在需要時帶入資料快取。
每週期多重作業
處理器通常會依照每秒可執行之百萬指令數(MIPS)計算其基準效能。不過,這對新型的處理器可能有誤導之嫌,因為指令的構成到底包含什麼還未有定論。例如,曾經只保留給較高成本的平行處理器使用的多重發佈(Multi-Issue)指令,現在也可提供低成本的定點處理器使用。除了在每個核心處理器週期執行多個ALU/MAC作業外,也可在相同的週期內完成額外的資料載入和存放作業。記憶體通常會分配到Sub-Bank中,可藉由核心或DMA控制器雙重存取。在考量上述的硬體位址計算後,可以明顯看出單一週期中可以進行許多作業。
圖5顯示多重運作指令的範例。如圖中所示,除了2個個別的MAC作業外,在同一處理器時鐘週期內還可完成資料擷取和資料存放作業。
連鎖管線
隨著處理器的速度增加,處理管線也必須變得越來越深,也就是更多階的管線。瞭解這點很重要,因為在需要組譯碼程式設計時,管線可能會使程式設計更複雜。不過某些處理器卻擁有「連鎖的」管線,這代表在執行組譯碼程式設計時,程式設計人員不需手動排定或追蹤透過管道移動的資料和指令,處理器會自動處理"停止(stall)"和"氣泡(Bubble)"。
彈性的資料暫存器列
最後,另一個補充功能是多用途的資料暫存集。在傳統的定點DSP中,字的大小通常是固定的。不過,如果可以將資料暫存器當做一個32位元的字(例如R0)或兩個16位元的字(上半和下半分別為R0.L和R0.H)來處理,也有其優點。在雙重的MAC系統中,這樣可在單一週期中,在四個16位元的資料片段上作業。
程式碼比較與分析
上述的架構是高效率DSP程式設計的基礎。如果程式設計人員能充分運用處理器的功能,許多到處可見的數字運算演算法都可以非常快速地執行。以下是一些常見的演算法,以及這些演算法在DSP上應該如何執行的說明。請注意,雖然應該就組譯碼層級檢視程式碼的效率,但經過最佳化的新型DSP編譯器,是為了運用可由組譯碼程式設計人員控制的許多相同規則而設計。範例中使用Blackfin處理器組合語言進行說明。
點積
點積 (或稱純量積) 是在測量兩向量正交時的有用作業。大多數的C程式設計人員都應該熟悉以下的點績實作:
short dot(short a[], short b[], int size) {
int i;
int output = 0;
for(i=0; i output += (a[i] * b[i]);
}
return output;
以下是此組譯程式碼的主要部分:
//P0=loop count, I0 & P1 are address registers
A1 = A0 = 0; // A0 & A1 are accumulators
LSETUP (loop1,loop1) LC0 = P0 ; // Set up hardware loop starting at label loop1
loop1: A1 += R1.H * R0.H , A0 += R1.L * R0.L || R1 = [ P1 ++ ] || R0 = [ I0 ++ ] ;
以下各點說明有助於撰寫此嚴謹程式碼的DSP架構功能。
硬體迴圈緩衝區和迴圈計數器能排除每個反覆運算結尾所需的跳躍指令:因為點乘積是積的總和,所以會在迴圈內實作。許多RISC微控制器會在每個反覆運算結尾使用跳躍指令,以處理迴圈的下次反覆運算。組譯碼程式顯示 LSETUP指令,這是實作迴圈所需的唯一指令。
多重發佈指令可在相同的週期中執行指令和兩次資料存取。在每次反覆運算中都必須讀取a[i]和b[i]值,然後將其相乘,最後寫回變數輸出的執行總和。在許多微控制器平台上,這樣可以有效地計為四個指令。組譯程式碼的最後一行顯示所有這些作業都可在一個週期內執行。
平行ALU作業可以讓兩個16位元指令同時執行。組譯程式碼顯示每個反覆運算中使用了兩個累加器單元(A0和A1)。這樣可減少50%的反覆運算數,有效削減一半的原始執行時間。
FIR
有限脈衝回應(FIR,Finite Impulse Response)濾波器 是等於迴旋作業的常見濾波器結構。簡單的C實作與點積非常類似:
// sample the signal into a circular buffer
x[cur] = sampling_function();
cur = (cur+1)%TAPS; // advance the cur pointer in a circular fashion
// perform the multiply-addition
y = 0;
for (k=0; k y += h[k] * x[(cur+k)%TAPS];
}
組譯碼中撰寫的FIR Kernel必要部分會顯示與點積類似的格式。事實上,同樣的DSP功能會用來為演算法的執行提供最高效能。在這個特定範例中,樣本會存放在R0暫存器中,而係數則存放在R1暫存器中。
// P0 holds # of filter taps
R0=[I0++] || R1=[I1++];
// set initial values for R0 and R1
A1=A0=0;
// zero the accumulators
LSETUP (loop1, loop1) LC0 = P0;
// configure inner loop
loop1: A1+=R0.L*R1.L, A0+=R0.H*R1.H || R0 = [I0++] || R1 = [I1++]; // compute
除了針對點積所描述的功能外,上述的FIR演算法還會運用循環式緩衝。
使用循環式緩衝即不必進行明確的模數算術(Modulus Arithmetic)。在C 程式碼片段中,%(模數)運算子提供循環式緩衝的機制。如組譯碼Kernel所示,此模數運算子不會在迴圈內轉譯為額外的指令,而是在迴圈外設定「資料位址產生器」暫存器I0和I1,以自動在達到係數緩衝區邊界時折疊回起始處。
FFT
快速富利葉轉換(Fast Fourier Transform)是許多訊號處理演算法的組成部分,其特點之一是輸入向量 Input Vector)是依時間順序排列,但輸出則是以位元倒置順序傳出。大多數傳統的一般用途處理器,都需要程式設計人員實作個別的常式,才能恢復位元倒置的輸出。在DSP平台上,位元倒置是設計在定址引擎內。
有了位元倒置定址,就不必在FFT實作內另外進行位元倒置的程序。因為硬體可以自動對FFT演算法的輸出進行位元倒置,所以程式設計人員不必撰寫額外的公用程式,效能因此而提升。
除了上述的指令建構外,某些處理器還包含額外的專用指令集,可支援多種應用程式。這些指令的用途是進一步擴充演算法的處理功能,例如Viterbi、Huffman程式碼撰寫和許多其他的位元處理常式。
結語
很明顯地,在針對DSP的應用程式定義程式設計策略時,要考量的事項很多。在大多數情況下,使用C或C++時配合強大的編譯器/最佳化程式可以產生相當健全的結果,但是手動建置的組譯碼是取得處理器額外效能的最佳方式。不過,只有在全盤瞭解提升程式碼撰寫效能的架構區塊後,才能進行這項手動作業。