TI C6000 數據存儲處理與性能優化
存儲器之於CPU好比倉庫之於車間。車間加工過程中的原材料、半成品、成品等均需入出倉庫,生產效率再快,如果倉庫周轉不善,也必然造成生產阻塞。如同倉庫需要合理地規劃管理一般,數據存儲也需要恰當的處理技巧來提升CPU的運算性能。
本文基於TI C6000系列DSP,介紹了與運算性能優化有關的存儲器知識。針對具體的數據存儲問題,給出相應的代碼優化策略,並將容易混淆的概念集中討論。
名詞說明
-
EMIF: External Memory Interface
-
PMC: Program Memory Controller
-
DMC: Data Memory Controller
-
SPC: Section Program Counter
存儲體沖突Vs存儲別名模糊[1]
1. 存儲體(bank)沖突
C6000系列各DSP的片內存儲器結構有所不同,其中大多數采用交叉存取式存儲體結構,如圖1所示,方框中數字表示字節地址。因為每個bank都是一個單口存儲器,所以每個周期對每個bank只能有一次訪問。例如兩個short型數據a和b,a存放在地址1-2中,b存放在地址8-9中,則程序中不能安排a和b並行存/取,否則會導致存儲器存取延遲,使流水線暫停一個周期,在暫停周期中進行第二次讀/寫存儲器,這就是存儲體沖突現象。
圖1 4bank交叉存取式存儲器
以16bit short型點積計算為例:
int dotp(short a[], short b[]);
一個高效的軟件流水核如下。在最後兩條指令中,用LDW“字加載”命令,一個周期中同時加載a[0]、a[1]、b[0]、b[1]。
要使軟件流水不被阻塞,則需保證數組a和b的並行加載不發生存儲體沖突。
-
沖突例子:a首地址=0;b首地址=8N
-
不沖突例子:a首地址=0;b首地址=8N+4
但是,完全控制數組和其它對象在存儲器空間的起始位置並不總是可以做到,尤其是當一個指針作為參數傳遞給函數時,調用這個函數的指針參數可能指向不同的存儲器位置。
如果不能知道數組a和b在存儲體中的排列信息,則只能肯定a[0-1]和a[2-3]不會發生存儲體沖突,同理於b[0-1]和b[2-3]。因此,可以通過循環展開的方式,安排a[0-1]和a[2-3]、b[0-1]和b[2-3]進行同時存取,避免了可能的存儲體沖突。循環展開後的軟件流水核如下:
另外,在線性匯編中,可以通過“.mptr”偽指令,給編譯器提供數據的存儲相關信息,讓編譯器自動分析是否會產生存儲體沖突並調整指令編排。
2. 存儲別名模糊(alias)
當多個不同的變量名都指向相同的存儲區域,這時就發生別名模糊,也就是說,對這些變量進行操作的指令可能存在存儲相關性。指令間的相關性限制了指令的編排,包括軟件流水編排。
匯編優化器假定所有的存儲器引用,都是別名化的(aliased),它把控制權交給用戶,由用戶提供存儲是否別名的信息。編程者可通過一個編譯選項/“restrict”關鍵詞/兩條線性匯編偽指令來提供存儲別名的信息。
-
-mt編譯選項:表示代碼中沒有存儲別名現象。要仔細判斷是否能使用-mt,如果代碼使用了別名技術而又設置了-mt選項,可能會出現意想不到的結果。
-
restrict關鍵詞:在C編程中,對數組或指針變量用restrict進行聲明,提示編譯器該變量指向的存儲區域不會與其它變量指向的存儲區域發生重疊。
-
.mdep偽指令:用於明確聲明存儲器相關。
-
.no_mdep偽指令:告訴匯編優化器函數體中沒有存儲器相關性出現。
*不要把“存儲別名模糊(存儲器相關)”和“存儲體沖突”兩個概念混淆,它們有著不同的含義和影響。別名模糊影響程序的正確性(當然也可能影響性能),存儲體沖突影響程序的性能。存儲器相關對於指令編排影響大於存儲體沖突。
存儲器模式Vs數據終結方式[1,2]
1. 存儲器模式
C6000編譯器支持兩種存儲器模式:小存儲器模式和大存儲器模式。
-
小存儲器模式:.bss段限制在32kB內,CPU可用直接尋址方式訪問.bss段中的所有對象而無需改變DP(B14)的值。
-
大存儲器模式:不限制.bss段大小,但CPU只能通過寄存器間接尋址訪問.bss中的數據,也即需要先將對象地址讀入寄存器中,這帶來額外的操作。
當全局/靜態變量(存放於.bss段)超過32kB,而又希望使用小存儲器模式獲得快的訪問速度,有兩種解決辦法:
-
對於大的數組定義,使用far關鍵字,如此數據不占用.bss段空間,而放入.far段。
-
使用-ml/-ml0選項,編譯器自動對集合數據類型(如結構體和數組)使用far存取。
2. 數據終結方式
指的是多字節數據內部高低有效位的存放順序。C6000支持兩種終結方式:小端終結方式和大端終結方式。
-
小端終結(Little-Endian):數據高有效位字節存放在地址高位字節(高位高地址)。
-
大端終結(Big-Endian):數據高有效位字節存放在地址低位字節(高位低地址)。
內存邊界對齊[1,3]
C67X DSP支持單次存/取16bit(半字)、32bit(字)、64bit(雙字)數據,但前提是數據的存放分別滿足半字對齊、字對齊和雙字對齊。目前C64X支持在非對齊情況下單次存/取32bit和64bit寬度的數據。
所謂半字對齊指的是數據地址的最低1位為0;字對齊指的是數據地址的最低2位為0;雙字對齊指的是數據地址的最低3位為0。
對不支持非對齊單次存/取的器件來說,如果讓CPU用多字節存/取指令一次操作非對齊的數據,將會產生額外操作,有些處理器甚至無法處理而產生錯誤!下圖給出了一個處理示例,從圖中可見,原本單次可以完成的操作由於數據未能對齊而花費了5次操作。
圖2 對非對齊數據進行多字節訪問
在C64X中,數組均默認安排8字節(雙字)對齊;在C62X和C67X中,數組按4字節/8字節對齊。
結構體的對齊方式由其最大數據類型成員決定,結構體占用的存儲空間總是最大成員類型大小的倍數(註意並不是簡單地乘以成員個數)。如下兩個結構體A和B,它們所占的存儲空間分別是8、12。
struct A { short x; short y; int z; }; struct B { short x; int y; short z; };
數據集邊界對齊並不意味著它裏面的每一個元素的地址都為對齊長度的倍數,而是保證數據集的起始地址和<結束地址+1>為對齊長度的倍數。
對齊的存儲器訪問
在C/C++代碼中,有三個pragma預編譯語句可以用來指示編譯器將具體的數據按指定的方式進行對齊存儲。
-
DATA_ALIGN:將數據進行2的整數次冪對齊
-
DATA_MEM_BANK:將數據對齊到指定的bank
-
STRUCT_ALIGN(C特有):用於指定結構體、聯合體進行2的整數次冪對齊
使用_nassert()內聯函數能夠指示編譯器某一數據的內存對齊狀態。如
_nassert( ((int)sum & 0x3) == 0);
告訴編譯器sum為字邊界對齊,有了這個信息,編譯器就可以放心地安排SIMD(單指令多數據)指令對數據進行操作,但_nassert本身不產生任何操作。
可以使用_amemXX()和_amemXX_const()內聯函數對對齊的字和半字進行訪問。一般這類內存訪問可以與_hi()、_lo()和_itod()等數據解包和打包內聯函數聯合使用。
非對齊的存儲器訪問
C64X支持非對齊的字和雙字訪問,其對邊界對齊和非邊界對齊數據訪問的比較如下表所示:
從上表可以看出,C64X在每時鐘周期只能進行一次非邊界對齊的存儲器訪問,因此,只要可能應盡量使用邊界對齊的存儲器訪問方式。
在C/C++代碼中,可以使用_memXX()和_memXX_const()內聯函數對非對齊的字和半字進行訪問。一般這類內存訪問可以與_hi()、_lo()和_itod()等數據解包和打包內聯函數聯合使用,下面是一個使用示例:
C6000 Cache(緩存)[4,5]
為什麽需要Cache?
大容量的存儲器(如DRAM)訪問速度受到限制,一般比CPU時鐘速度慢很多;小容量的存儲器(如SRAM)能提供快速的訪問速度。因此很多高性能的處理器都提供分層的存儲訪問架構。
如圖3所示,左右分別是平坦式存儲器架構和2層cache的多層存儲架構。在左邊的架構中,即使CPU能運行在600MHz,但由於片內/片外存儲器只能運行在300MHz/100MHz,CPU在訪問存儲時需插入等待周期。
圖3 平坦式和層級式存儲器架構
Cache部分工作狀態說明
-
Cache hit(緩存命中):對於已經緩存的程序/數據,訪問將引起緩存命中,緩存中的指令/數據立即送入CPU而無需等待。
-
Cache miss(緩存缺失):發生缺失時,首先通過EMIF讀入需要的指令/數據,指令/數據在送入CPU的同時被存入Cache,讀入程序/數據的過程CPU被掛起。
-
Cache flush(緩存命中):清空Cache已經緩存的數據。
-
Cache freeze(緩存凍結):Cache內容不再改變,發生缺失時,從EMIF中讀入的指令包不會同時存入Cache。
-
Cache bypass(緩存旁路):Cache內容不再改變,任何程序/數據都將從緩存外存儲器訪問。
C6000的存儲架構
C6000系列DSP在片內RAM和CPU之間提供兩層Cache L1和L2,每層Cache又分為獨立的程序Cache和數據Cache。其中L1是固定的,L2可以被重映射為普通片內RAM。
對程序/數據進行訪問時,CPU首先到L1 Cache中尋找,命中則直接訪問,如果產生缺失,則繼續在L2Cache中尋找,如果還未命中,則到片內RAM或片外RAM中尋址數據。
圖4 C6000 CPU的程序/數據訪問流程
訪問定位的規律
由圖4可知,要保證CPU的存儲訪問效率,只有在CPU在大部分的訪問都是只針對最靠近它的存儲區時才有效。幸運的是,根據訪問定位的規律,這一條可以保證。訪問的定位規律表明程序在一個相對小的時間窗口對僅需要一個相對較小size的數據和代碼。數據定位的兩條規律:
-
空間關聯性:當一個數據被訪問時,它臨近的數據又很大可能會被後續的存儲訪問。
-
時間關聯性:一個存儲區被訪問時,在下一個臨近的時間點還會被訪問。
優化cache性能
從訪問定位規律出發,可總結出優化cache性能的一些基本準則:
-
讓函數盡可能充分的對數據處理以提高數據的重用。
-
組織數據和代碼以提高cache命中率。
-
合理的空間劃分來平衡程序cache和數據cache。
-
組合那些對相同數據進行處理的函數在一個存儲區域。
段[1,6]
目標文件(.obj)的最小單位稱為段,它是占據一個連續空間的代碼塊或者數據塊。連接器的功能之一就是把段重定位到目標系統的存儲器映像圖中。所有段都可以獨立重定位,用戶可以把任一段置入目標存儲器任一指定塊內。
一個COFF文件包含三個默認段:.text、.data、.bss。用戶還可以創建、命名、連接自己的段,也可以繼續在各個段中繼續劃分子段。
在C/C++代碼中,有兩個預編譯語句可用來將特定的代碼或數據分配到指定的段中:
-
CODE_SECTION:為代碼分配段。
-
DATA_SECTION:為數據分配段。
棧和堆[1,6]
棧(.stack)和堆(.heap)是為處理器運行時提供支持的兩個存儲區。
棧是由編譯器在需要時分配的,不需要時自動清除的變量存儲區。它用於存放局部變量、函數參數等臨時數據。
堆用於動態內存分配。堆在內存中位於bss區和棧區之間。一般由程序員分配和釋放,若程序員不釋放,程序結束時有可能由OS回收。例如C中常用的malloc()函數就是在堆中開辟區間存放數據。
參考文獻/資料
【1】田黎育,何佩琨,朱夢宇. TMS320C6000系列DSP編程工具與指南[M].北京:清華大學出版社 2006.
【2】董言治,婁樹理,劉松濤. TMS320C6000系列DSP系統結構原理與應用教程[M]. 清華大學出版社, 2014.
【3】Data alignment: Straighten up and fly right - IBM developerworks.
【4】Cache memory – Wikipedia.
【5】如何優化使用C6000系列C64x的Cache--原理,Cache種類和優化策略 - nouth的網易博客
【6】C語言中內存分配 - youoran的CSDN專欄.
·END·
想進一步跟蹤本博客動態,歡迎關註我的個人微信訂閱號:信號君
信號君:尋求簡單之道
技術成長 | 讀書筆記 | 認知升級
掃描二維碼關註信號君
TI C6000 數據存儲處理與性能優化