毫秒必爭之如何搞定cache(上)


 

本文以SmartPro 6000F使用的nios ii內核為例,詳述了如何搞定cache,將程序的運行時間從最開始的30s優化到25s,再從25s優化到最終的24s。尤其是那最后1s的優化,遇到了很多問題,而這些問題在嵌入式系統里,任何一款配置了cache的處理器都可能會碰到,所以特撰此文獻給那些還在倍受cache折磨的工程師們。全文分上下兩部,上部為如何搞定指令cache,下部為如何搞定數據cache。

 
SmartPro 6000F使用全FPGA架構,並內嵌了4顆nios II軟核,我們使用的開發環境是nios for eclipse,編譯器是gcc。在幫客戶定制一款nandflash編程時序時,為了進一步提高編程速度,准備對時序進行優化。默認情況下,gcc的優化等級為最低的O0,編程時間為30s,開啟O1一級優化后,編程時間降到了25s,當開啟O2二級優化后,編程時間沒有繼續下降,反而又上升到了29s。
 
雖然不同優化等級之間時間差別就不到5s,我們完全可以很省事的開啟O1級別優化時序,然后就交付給客戶,但是當時我們並沒有這么做。因為理論上,優化等級越高,編程時間應該越少才對,但是現在測試的編程時間結果是 O0 > O2 > O1,冥冥中感覺時序還可以再優化些,速度還可再快一點。
 
目前提速遇到的問題是:使用更高的優化等級,編程時間反而更多了,這可不符合gcc的優化規律阿。那是什么的存在破壞了優化規律呢?宏觀的考慮,在嵌入式系統里,能破壞程序運行規律的家伙,嫌疑最大的就是cache了。
 
cache存在的初衷是為了提速,因為程序指令如果完全運行在內存里,速度會非常慢,而在cache里運行將非常快,但是cache的容量是有限的,無法緩存所有程序,所以nios ii內核在設計的時候做了一個折中處理,先將內存里的程序搬運到cache里,然后在cache里運行程序,由於cache無法一次性緩存所有程序,如果運行的程序大小超過了cache容量,必須要重新訪問內存更新cache,如果更新頻率越高,則訪問內存的次數就越多,運行效率自然就會被拉低,而cache的更新頻率是不可預測的,所以配置了cache的嵌入式系統的運行時間一般都很難預測。
 
但是,SmartPro 6000F里的每一個nios ii內核都配置了4KB的指令cache和2KB的數據cache,而編程時序只有不到2KB,理論上完全可以緩存到cache里運行,根本不用去更新cache,也就不應該存在運行不規律的問題了。
 
但是,但是,上述只是我們的一個理論分析,實際運行到底有沒有更新cache,還需實際測試說了算。我們將6000F的內存sram的讀(rd)、寫(wr)和地址(addr)連上邏輯分析儀觀察,如果rd線或者wr線有出現脈沖,就說明存在訪問內存更新cache的操作。不同的優化等級下,測試波形如下所示:
 
O1   25s
O2   29s
O0   30s
 
由上圖發現,運行時間較長的,都不同程度的出現了訪問內存的現象,而且訪問的越頻繁,速度越慢。編程時序的大小已經完全可以加載到cache里運行,為什么還會訪問內存呢?
 
看來cache的加載方式好像沒有之前想的那么簡單,為了解決這個疑惑,讓我們再來研究下在O2優化等級時編程時序的匯編代碼。編程函數Program在內存sram的地址分布區域從0x1014到0x15F0,里面會調用到的一個定時器函數Timer在內存的分布從0x2020到0x2170。
 
Program( )
{
0x1014
0x1018
0x101c    call Timer( )
…
0x15F0
}
…
Timer( )
{
0x2020
0x2024
...
0x2170
}

 

之前天真的以為在運行Program時,這兩個函數會按照如下所示順序的加載到cache里。

 
cache_addr     sram_addr
0x0             0x1014                Program( )
0x4             0x1018             
0x8             0x101c
 0xc             0x2020                Timer( )
 0x10            0x2024
 0x14            0x2028
 ...
 0x5c            0x2070
 0x60            0x1020
 ...
 0x62c           0x15f0

 

研究了一下cache的數據結構后發現,數據並不是簡單的順序存儲在cache里,不同原理的cache,使用的數據結構也不同,從nios ii開發手冊里獲知,當前平台使用的是直接映射結構的cache,數據以散列的格式存儲,為了簡化和提高cache的效率,nios ii 里的cache利用了一個最簡單的散列函數:
 
cache_addr = sram_addr  mod  cache_size

 

其中cache_addr為cache地址,sram_addr為內存sram地址,cache_size為cache大小,這里為4K,所以Program和Timer函數在cache里的實際存儲格式是
 
 
cache_addr  Program( )    Timer( )
0x0            
...
0x14          0x1014             
0x18          0x1018
0x1c          0x101c
0x20          0x1020       0x2020
0x24          0x1024       0x2024
...            ...
0x70          0x1070       0x2070
0x74          0x1074
...
0x5F0         0x15f0

 

由上可以看到,cache里,從0x20地址開始,Program和Timer的加載發生了沖突。編程時序運行時,cache里首先存的是Program,當運行到Timer時,nios ii會從內存調取Timer的函數存入0x20開始的cache,並覆蓋Program的一部分函數,當Timer執行完后,繼續運行Program,nios ii又要從內存獲取Program中被覆蓋的那部分程序,調入cache里執行,這樣每執行一次Program函數,就會更新兩次cache。
 
到這里,所有問題似乎都豁然開朗了,不同等級的優化設置后,在改變函數大小的同時,也會改變它們在內存的地址分布。Program和Timer的分布地址,通過cache的散列后如果沒有沖突,那么在運行時就不會訪問內存,如果產生了沖突,並且沖突的地址越多,則訪問內存的時間就會越長,整體速度就會越慢。O2的優化等級比O1高,雖然前者優化后的程序更小,但是前者在cache的散列加載地址發生了沖突,速度自然就更慢了。
 
要解決沖突問題,必須從cache的散列函數入手。
 
方法一:增大cache的容量
由於Program和Timer的函數分布地址跨度過大,超過了cache_size,才導致散列后發生沖突,如果將cache_size增大到8KB,Program在cache里的加載地址是0x1014,Timer在cache里的加載地址是0x20,不會發生沖突。但是嵌入式系統里的資源都非常精貴,很多系統無法提供這么大的cache,此時可以采用另一種更實惠的方法。
 
方法二:通過分散加載,將Program和Timer的分布地址跨度縮小到cache_size內
在bsp里,新增一個從0x1000到0x2000的段.UserCache,然后將Program和Timer強制分布到這個段里,這樣兩個函數在cache里的存儲地址也不會沖突了。gcc里,將函數到分布到指定的段的語法如下:
 
int  Program( ) __attribute__ ((section(“.UserCache ")))

 

通過方法二的改進后,不同優化等級下Program的運行時間變成了:O0 30s, O1 25s,O2 24s,又比之前縮短了1s。
 
以前一直聽說加入cache后,程序的運行就會變的不可預測,這次算是徹底的感受到了,但不可預測並不代表不可控,通過上述的兩種方法,就可以控制函數盡量不去訪問內存,提高執行效率和運行的一致性。
 
但是好景不長,在給客戶增加了一個小功能后,cache又犯毛病了,不過這次出問題的不是指令cache,而是數據cache,詳文請看“毫秒必爭之如何搞定cache(下)”。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM