其實(shí)我不是很會(huì )寫(xiě)文章,想要把技術(shù)性文章寫(xiě)的有意思就更難了。不過(guò)這一段日子總是有一種沖動(dòng)想要寫(xiě)點(diǎn)什么,把自己了解的有關(guān)BlackfinC語(yǔ)言優(yōu)化和系統優(yōu)化方面的技巧和知識寫(xiě)下來(lái),和正在從事這方面工作朋友們分享,也許有些幫助,也算是對自己過(guò)去一段時(shí)間工作的總結。 在文章開(kāi)始之前,我想先問(wèn)讀者一個(gè)問(wèn)題:您的DSP代碼里有多少是匯編,這些匯編里有多少是您自己寫(xiě)的? 曾幾何時(shí)匯編編程是DSP工程師的一張名片。很多人到現在談起匯編編程還是頗為自豪的,搞得你想說(shuō)自己不會(huì )都要鼓起點(diǎn)勇氣——那眼神是恨不得把你送回火星去。這主要是因為在最開(kāi)始的時(shí)候DSP上的C語(yǔ)言編譯器不是很普遍,編譯器的水平也還在起步階段,很難用到DSP相應的硬件特性,編譯效率值得商榷。而且那時(shí)DSP應用場(chǎng)景和復雜度遠不比今天,基本上限制在數字信號處理的典型算法上,FFT,FIR,IIR濾波器,等等。這些函數和濾波器的實(shí)現相對今天的應用比較簡(jiǎn)單,用匯編語(yǔ)言也容易突出DSP的硬件特性。還有一個(gè)原因是那時(shí)候DSP普遍都跑的很慢,基本上在幾十兆的水平。這也限制了C語(yǔ)言的使用。試想一下一段C代碼跑的比匯編慢十倍,幾十兆的DSP一下就變幾兆了。 但是今天再來(lái)看這所有的一切是完全不一樣了。首先是DSP的應用范圍越來(lái)越廣,客戶(hù)越來(lái)越多的希望用同一顆芯片,在同一個(gè)平臺上實(shí)現更多的設計和應用。這對DSP的設計,DSP和MCU的融合都帶來(lái)重大影響。DSP和MCU之間也不是過(guò)往那井水不犯河水的安寧。隨著(zhù)DSP和MCU的主頻先后突破1GHz,在很多應用中DSP和MCU相伴相生的場(chǎng)景也開(kāi)始被一顆強壯的芯代替,或者DSP或者M(jìn)CU。在這樣的應用中,操作系統,文件系統,USB協(xié)議棧,TCP/IP,海量數據存儲,樣樣都會(huì )用到。數字信號處理也從骨灰級的濾波器變成全系列音視頻處理,OFDM基帶處理,天線(xiàn)陣列信號處理,彩色圖像重建…試想一下這些應用哪一個(gè)不是成千上萬(wàn)行代碼。匯編語(yǔ)言在編程復雜度,可移植性和可維護性上真的是遇到了前所未有的挑戰。而與此相對應的是C語(yǔ)言和C語(yǔ)言編譯器的蓬勃發(fā)展。今天您可以很容易找到上面提到所有這些應用和算法的C語(yǔ)言實(shí)現,而C語(yǔ)言編譯器在編譯效率和成熟度上都有很大的突破。也讓C語(yǔ)言在DSP上的應用得以受到愈來(lái)愈高的重視。 但是C語(yǔ)言本身并不是為DSP定義的——C語(yǔ)言在PC上的默認條件在嵌入式處理器上不成立,比方說(shuō)存儲空間無(wú)限,比方說(shuō)內存連續,更不要說(shuō)如何綁定DSP特殊的硬件支持。所以要充分發(fā)揮DSP的能力,C語(yǔ)言?xún)?yōu)化是一下一張DSP工程師的名片。不會(huì )C語(yǔ)言?xún)?yōu)化,OK,你可以回火星,地球很危險。 1. 拳譜總綱 閑話(huà)不表。在深入到細節之前,我想先從宏觀(guān)的角度討論一下C語(yǔ)言?xún)?yōu)化一些大的原則。就好像我們在學(xué)七傷拳之前先來(lái)背背拳譜總綱,提綱挈領(lǐng)很重要。這些原則可以用圖1來(lái)說(shuō)明。 圖1:C語(yǔ)言?xún)?yōu)化性能曲線(xiàn)。 對整條性能曲線(xiàn)可以做這樣的總結,1)最佳性能產(chǎn)生在C和匯編按一定比例分配的情況下,80-20可以作為一個(gè)參考;2)將所有代碼都轉為匯編并不會(huì )帶來(lái)性能的進(jìn)一步提高;3)在C語(yǔ)言編譯器的幫助下,將大多數控制代碼保留在C語(yǔ)言范疇中是可能的;4)要想達到最佳性能,那些消耗cycle最多的代碼應轉化為C語(yǔ)言可以調用的匯編函數。簡(jiǎn)單說(shuō)就是讓C和匯編語(yǔ)言做各自擅長(cháng)的事情,在動(dòng)態(tài)平衡中達到最佳性能。內事不決問(wèn)張昭,外事不決問(wèn)周瑜,各司其職。 在DSP性能大幅提高的今天,如果可以如圖中B點(diǎn)那樣用Optimized C將C語(yǔ)言在DSP上的性能提高到%70以上,很有可能對于大多數應用場(chǎng)景就已經(jīng)足夠了,并不是一定要接觸匯編語(yǔ)言的。這個(gè)從A點(diǎn)到B點(diǎn)的過(guò)程也正是這篇文章要討論的重點(diǎn)。 2. 是騾子是馬您先別溜 說(shuō)到這里,有很多朋友等不及要開(kāi)始做優(yōu)化了:打開(kāi)程序,一條語(yǔ)句、一條語(yǔ)句立刻看起來(lái)。很多時(shí)候我們在工作中都遇到這樣的情況,所以第一刻就要喊停,等我先講講一些容易被忽略的東西。 首先最容易被忽略的是數據類(lèi)型。通常編譯器對ANSIC所有數據類(lèi)型都是支持的,但是硬件呢,是不是對所有的數據類(lèi)型都很有效的支持呢?舉個(gè)例子,很多DSP都有專(zhuān)門(mén)針對16-bit定點(diǎn)運算的指令,特別是一些并行指令。如果在算法中可以將數據類(lèi)型設計為16-bit就可以充分利用到這些指令。Blackfin每個(gè)cycle可以做2個(gè)16-bit乘法,而每個(gè)32-bit乘法則要消耗3個(gè)cycle。這中間有6倍的差距,是值得我們考慮的。另外定點(diǎn)芯片不直接支持浮點(diǎn)操作,如果算法中有浮點(diǎn)類(lèi)型和浮點(diǎn)運算,則首先應該考慮在不影響動(dòng)態(tài)范圍和精度的基礎上進(jìn)行定點(diǎn)化。因為在定點(diǎn)芯片上每個(gè)浮點(diǎn)操作都可能消耗成百上千個(gè)cycle來(lái)得到近似的結果。對于小數類(lèi)型,Blackfin直接支持1.15和1.31小數類(lèi)型的操作,這給程序員很大的靈活度。所以我們首先要盡可能依托當前DSP最擅長(cháng)的操作來(lái)確認數據類(lèi)型被支持的程度,并對算法進(jìn)行調整。 另一個(gè)容易被忽略的地方是算法本身。也就是被采用的算法本身是不是已經(jīng)是最高效,最優(yōu)的?紤]一下正在用的排序算法是不是還有余地改進(jìn);要用的正弦波形是計算還是查表;又或者整個(gè)算法或者部分可以被更高效的算法代替。這樣的考慮往往可以達到事半功倍的效果,就好像換了三趟公交去看朋友,下車(chē)一抬頭發(fā)現有條地鐵直達。 在現代高性能DSP中通常都有比較深的指令流水線(xiàn)。流水線(xiàn)的作用是把一個(gè)cycle里要做的事情分在多個(gè)步驟里來(lái)做。對于高主頻的芯片而言,流水線(xiàn)的深度是很重要的,它從某種程度上決定了可能的最高主頻速度。每一個(gè)節拍,指令流水線(xiàn)上不同功能單元同時(shí)并行運作,每條指令按順序流經(jīng)這些功能單元?上挛锟傆袃擅嫘,當流水線(xiàn)遇到了條件跳轉,它的另外一面就充分暴露出來(lái)了。那就是在跳轉的時(shí)候,當前指令之后已經(jīng)在流水線(xiàn)里的指令全部都要被清空,然后再讓要跳轉到的目的指令重新進(jìn)入流水線(xiàn)。如果流水線(xiàn)的深度是N,那么這里損失的cycle通常為N-1。流水線(xiàn)越深,損失越大。如果不巧這個(gè)條件跳轉在循環(huán)里面,這個(gè)N-1的損失就會(huì )被放大了。用一些方式替代條件跳轉可以減輕這樣的損失,比方說(shuō)盡可能的使用條件執行和條件賦值,或者max和min語(yǔ)句,因為這些語(yǔ)句的執行通?梢栽贒SP的匯編級找到對應的單周期語(yǔ)句。另外就是要盡可能的避免在循環(huán)中使用條件跳轉。 除法運算是我們需要注意的一種操作,因為通常除法在DSP中都是一段近似算法來(lái)實(shí)現的。比如說(shuō)在Blackfin提供兩種除法近似,精度較低的一種需要大約40 cycle而32bit除法則需要大致400cycle。想想一個(gè)1000次的for循環(huán)里如果有3次除法,您就大致知道您的程序會(huì )跑多慢了。所以我們要在算法中考慮到除法的影響和可能的替代方式,例如利用不等式原則可以把除法變成乘法,又或者模2的除法可以變成移位。當然了,我在這里提到的替代,包括針對前面的數據類(lèi)型,算法和條件跳轉,都是遵循“盡可能”的原則,沒(méi)有絕對的意思。優(yōu)化的后程序效率的高低就是體現在這個(gè)盡可能上。 3. 編譯器,睡在上鋪的兄弟 這一刻,你不是一個(gè)人在戰斗…,這話(huà)聽(tīng)起來(lái)好像有點(diǎn)耳熟。如果把C語(yǔ)言?xún)?yōu)化比作是程序員在進(jìn)行的一場(chǎng)戰斗的話(huà),程序員并不孤獨,因為我們有一個(gè)隱形的戰友,就是編譯器,而編譯器的優(yōu)化功能就是我們最有力的武器。以VisualDSP++為例,通常新建的工程C語(yǔ)言?xún)?yōu)化缺省是不打開(kāi)的,程序員可以按照程序運行的需要打開(kāi)優(yōu)化。這個(gè)從不優(yōu)化到優(yōu)化的過(guò)程實(shí)際上反映了VisualDSP++編譯器在處理C語(yǔ)言程序過(guò)程中的兩步走。 在優(yōu)化開(kāi)關(guān)沒(méi)有打開(kāi)的情況下,編譯器對C代碼的處理是一一對應的直譯,就是把C代碼一句一句按照先后順序翻譯為相應的一條或者多條匯編語(yǔ)句。在直譯的同時(shí),編譯器也會(huì )注意到對中間變量和中間結果的保護——不管他們接下來(lái)會(huì )不會(huì )被用到,他們都會(huì )被寫(xiě)入存儲器,盡管這樣做會(huì )增加很多冗余。經(jīng)過(guò)這樣的直譯,一段C代碼對應的匯編代碼可能是多一個(gè)數量級的。一個(gè)典型的例子是,只有兩條乘累加指令的for循環(huán)代碼對應的匯編代碼是幾十條之多?上攵,這樣不經(jīng)優(yōu)化的代碼執行速度是很慢的。一個(gè)參考數據是打開(kāi)優(yōu)化開(kāi)關(guān)以后的代碼運行速度平均可以提高20倍。也就是說(shuō),一個(gè)600MHz的芯片,不打開(kāi)優(yōu)化,相當于主頻降到30MHz。所以絕大多數情況下我們要打開(kāi)編譯器的優(yōu)化開(kāi)關(guān)。 編譯器的第二步走,就是對直譯產(chǎn)生的代碼進(jìn)行優(yōu)化,這個(gè)過(guò)程就是充分利用DSP的硬件實(shí)現指令和事件最大可能并行的過(guò)程。這里的并行既有運算單元本身的并行也有運算單元和其他功能單元的并行。以Blackfin為例,每一個(gè)core里都有兩個(gè)乘法器和加法器。編譯器在優(yōu)化的時(shí)候第一個(gè)層次的并行是運算的并行,就是盡可能同時(shí)使用兩個(gè)運算單元,做乘法就盡可能做到兩個(gè)乘法器同時(shí)運算,做加法就盡可能做到兩個(gè)加法器同時(shí)運算。接下來(lái)一個(gè)層次的并行是指令的并行,就是運算單元和memory存取、或者其他功能單元之間的并行,仍以Blackfin為例,在同一個(gè)cycle中,可以有兩個(gè)乘累加和兩個(gè)數據的存或取并發(fā)執行。這些并行都是DSP硬件本身支持的,編譯器優(yōu)化的工作就是充分利用DSP的硬件能力。 循環(huán)是編譯器在第二步走的過(guò)程中重點(diǎn)處理的對象。這比較好理解,因為那些大量消耗cycle的代碼往往是在循環(huán)當中的。下面我就結合編譯器對循環(huán)的處理,來(lái)看看在優(yōu)化的過(guò)程中程序員要怎么和編譯器并肩戰斗。編譯器對循環(huán)處理的目標就是希望在每一次循環(huán)中盡可能的并行。為了實(shí)現這個(gè)目標,編譯器采取的措施就是不停的打開(kāi)循環(huán)、降低循環(huán)次數,增加循環(huán)內的指令個(gè)數,提高指令之間并發(fā)的幾率。舉個(gè)例子,一個(gè)100次的循環(huán)中有一個(gè)乘累加,編譯器打開(kāi)循環(huán),將循環(huán)次數降低一半,循環(huán)內每次就會(huì )出現兩個(gè)乘累加,編譯器就有可能安排Blackfin的兩個(gè)乘累加單元同時(shí)運算,從而將執行的效率提高一倍,這個(gè)優(yōu)化過(guò)程叫做矢量化(Vectorization)。如果這個(gè)循環(huán)中還有加法、減法、存數、取數,或者其他運算,編譯器還會(huì )安排這些指令和乘累加并發(fā),或者這些指令之間并發(fā),這個(gè)優(yōu)化的過(guò)程也是實(shí)現軟件流水線(xiàn)的過(guò)程(SoftwarePipeline)——在優(yōu)化后的代碼中往往出現當前的運算和以往的存數或者未來(lái)的取數并行。編譯器對循環(huán)的打開(kāi)可能是多次的,直到編譯器有足夠的指令可以充分安排并發(fā)。 說(shuō)到這里我們對這位睡在上鋪的兄弟已經(jīng)有一些了解了,那么程序員在這個(gè)優(yōu)化的過(guò)程中應該做什么呢?這就要從矢量化和軟件流水線(xiàn)受到的限制談起。剛才提到在優(yōu)化過(guò)程中編譯器一個(gè)重要的操作就是打開(kāi)循環(huán),如果循環(huán)次數是2的N次方例如8,16,32…,編譯器就可以很舒服的按照需要多次打開(kāi)循環(huán)。但如果在上面的例子里循環(huán)次數是101,編譯器是無(wú)法打開(kāi)循環(huán)的,對這個(gè)循環(huán)的優(yōu)化就不能有效的展開(kāi)。這個(gè)時(shí)候就需要程序員做工作了:我們可以將循環(huán)里面的運算在循環(huán)外實(shí)現一次,讓循環(huán)次數變?yōu)?00,從而給編譯器兩次打開(kāi)循環(huán)的機會(huì )(2x2x25)。矢量化和軟件流水線(xiàn)對操作數的存放也是有要求的。首先,對memory中操作數讀取和計算結果存放必須是順序(地址遞增或者遞減)的,如果是亂序或者隨機的,不管是運算的并行和是指令的并行都很難實(shí)現。我們在編寫(xiě)程序和對C程序進(jìn)行優(yōu)化的時(shí)候就要注意到盡可能安排數據訪(fǎng)問(wèn)的順序性。其次,根據操作數的寬度,程序員還要注意保證數據的2字對齊或者4字對齊。這有助于在指令并行執行時(shí)對操作時(shí)的有效讀取。程序員可以通過(guò)在定義數據(組)的時(shí)候用編譯器提供的相應編譯選項來(lái)實(shí)現數據的對齊。在進(jìn)行矢量化和軟件流水線(xiàn)的過(guò)程中往往要對程序執行的順序做局部調整,這種調整對程序整體來(lái)說(shuō)雖然是微調,但在某些情況下改變原始程序執行的順序會(huì )影響到程序執行結果的正確性。最典型的情況就是運算的操作數和結果之間存在某種聯(lián)系和依賴(lài)。比方說(shuō)數組中靠后的成員數值取決于靠前的成員運算的結果,這意味著(zhù)數組成員之間有依賴(lài)性,不獨立,從而不能實(shí)現并行計算。這在for循環(huán)中經(jīng)常體現為一個(gè)運算的兩個(gè)操作數指針可能是指向同一個(gè)數組的不同位置。數據獨立性是到目前位置我們看到影響客戶(hù)C代碼優(yōu)化效率最嚴重的因素。 編譯器在進(jìn)行優(yōu)化的時(shí)候永遠都遵循一個(gè)基本原則,那就是優(yōu)化不能影響程序運行的正確性。所以當編譯器發(fā)現矢量化和軟件流水線(xiàn)需要滿(mǎn)足的那些條件不確定的時(shí)候,它的行為往往是保守的。這是一種寧可放棄性能也要保證正確性的態(tài)度,無(wú)可厚非。該出手時(shí)就出手,到了程序員幫編譯器一把的時(shí)候了。因為編譯器面對的這些不確定性,在程序員看來(lái)通常是確定,一定,以及肯定的。以前面數據獨立性的問(wèn)題為例,編譯器很難判斷當前for循環(huán)中兩個(gè)指針pa,pb在運行的時(shí)候是不是會(huì )指向同一個(gè)數組,因為對編譯器來(lái)說(shuō)它們只是兩個(gè)指針,對它們后面實(shí)際操作的對象毫無(wú)頭緒。而程序員卻可能清楚的知道這段程序處理的兩個(gè)數組是定義在兩段不同的物理內存上的,也就是說(shuō)這兩個(gè)指針不會(huì )指向同一段地址,數據的獨立性是有保證的。這個(gè)時(shí)候我們就可以通過(guò)相應的編譯選項通知編譯器:下面這個(gè)for循環(huán)里的數據是獨立的,放心大膽的優(yōu)化吧。這里提到的編譯選項,包括前面說(shuō)的關(guān)于循環(huán)次數,數據對齊,以及存儲位置等其他編譯選項都可以在VisualDSP++關(guān)于C語(yǔ)言編譯器的手冊中找到。 了解了編譯器的工作方式,針對矢量化和軟件流水線(xiàn)對代碼和數據存儲的要求,在C語(yǔ)言范圍內對相關(guān)代碼進(jìn)行調整,并通過(guò)編譯選項將有利于優(yōu)化的確定信息通知編譯器,依托C語(yǔ)言編譯器的能力實(shí)現代碼的高效優(yōu)化,就是程序員在這里要做的工作。 4. 打完收工,還是剛剛開(kāi)始 我們已經(jīng)簡(jiǎn)單的談了C語(yǔ)言?xún)?yōu)化,特別是性能曲線(xiàn)從A點(diǎn)到B點(diǎn)應該遵循的主旨和一些技巧。個(gè)人認為,嵌入式系統上高效的C代碼優(yōu)化不是在代碼寫(xiě)好以后才開(kāi)始的一個(gè)獨立的步驟,而應該是在系統設計和編寫(xiě)代碼的時(shí)候就已經(jīng)開(kāi)始考慮硬件平臺有效執行的因素,妥善安排算法,精度,數據類(lèi)型,存儲空間和性能之間的關(guān)系。再加上靈活應用上面提到的技巧,可以做到事半功倍。由于篇幅的限制,這里只能提綱挈領(lǐng)的講一講。有興趣的讀者可以訪(fǎng)問(wèn)http://www.analog.com/zh/embedde ... ng/fca.html#ADEV001 到此為止,C語(yǔ)言?xún)?yōu)化告一段落,而嵌入式系統的優(yōu)化才剛剛開(kāi)始。片內片外代碼和數據的分配,主頻和外頻的選擇,系統帶寬和DMA的使用,這些都會(huì )影響到優(yōu)化后的代碼在嵌入式系統里最終的性能;鹦侨说牡厍蛑,才剛剛開(kāi)始。 |