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