編譯器管理的存儲器
《深入理解計算機系統》p422
6.1 存儲器層次結構中的緩存
一般而言,高速緩存( cache ,讀作「 cash 」)是一個小而快速的存儲設備,它作為存儲在更大、也更慢的設備中的數據對象的緩沖區域。使用高速緩存的過程稱為緩存( caching ,讀作「 cashing 」)。存儲器層次結構的中心思想是,對於每個 k ,位於 k 層的更快更小的存儲設備作為位於 k 十1層的更大更慢的存儲設備的緩存。換句話說,層次結構中的每一層都緩存來自較低一層的數據對象。例如,本地磁碟作為通過網路從遠程磁碟取出的文件(例如 Web 頁面)的緩存,主存作為本地磁碟上數據的緩存,依此類推,直到最小的緩存—— CPU 寄存器組。圖6-22展示了存儲器層次結構中緩存的一般性概念。第 k 十1層的存儲器被劃分成連續的數據對象組塊( chunk ),稱為塊( block )。每個塊都有一個唯一的地址或名字,使之區別於其他的塊。塊可以是固定大小的(通常是這樣的),也可以是可變大小的(例如存儲在 Web 伺服器上的遠程 HTML 文件)。例如,圖6-22中第 k 十1層存儲器被劃分成16個大小固定的塊,編號為0~15。
類似地,第 k 層的存儲器被劃分成較少的塊的集合,每個塊的大小與 k 十1層的塊的大小一樣。在任何時刻,第 k 層的緩存包含第 k 十1層塊的一個子集的副本。例如,在圖6-22中,第 k 層的緩存有4個塊的空間,當前包含塊4、9、14和3的副本。
數據總是以塊大小為傳送單元( transfer unit )在第 k 層和第 k +1層之間來回復制的。雖然在層次結構中任何一對相鄰的層次之間塊大小是固定的,但是其他的層次對之間可以有不同的塊大小。例如,在圖6-21中,L1和 LO 之間的傳送通常使用的是1個字大小的塊。L2和L1之間(以及I3和I2之間、L4和I3之間)的傳送通常使用的是幾十個位元組的
塊。而L5和L4之間的傳送用的是大小為幾百或幾千位元組的塊。一般而言,層次結構中較低層(離 CPU 較遠)的設備的訪問時間較長,因此為了補償這些較長的訪問時間,傾向於使用較大的塊。
1. 緩存命中
當程序需要第 k 十1層的某個數據對象 d 時,它首先在當前存儲在第 k 層的一個塊中查找 d 。如果 d 剛好緩存在第 k 層中,那麼就是我們所說的緩存命中( cache hit )。該程序直接從第 k 層讀取 d ,根據存儲器層次結構的性質,這要比從第 k +1層讀取 d 更快。例如,一個有良好時間局部性的程序可以從塊14中讀出一個數據對象,得到一個對第 k 層的緩存命中。
2. 緩存不命中
另一方面,如果第 k 層中沒有緩存數據對象 d ,那麼就是我們所說的緩存不命中( cache miss )。當發生緩存不命中時,第 k 層的緩存從第 k 十1層緩存中取出包含 d 的那個塊,如果第 k 層的緩存已經滿了,可能就會覆蓋現存的一個塊。
覆蓋一個現存的塊的過程稱為替換( replacing )或驅逐( evicting )這個塊。被驅逐的這個塊有時也稱為犧牲塊( victim block )。決定該替換哪個塊是由緩存的替換策略( replace — ment policy )來控制的。例如,一個具有隨機替換策略的緩存會隨機選擇一個犧牲塊。一個具有最近最少被使用 LRU )替換策略的緩存會選擇那個最後被訪問的時間距現在最遠的塊。
在第 k 層緩存從第 k 十1層取出那個塊之後,程序就能像前面一樣從第 k 層讀出 d 了。例如,在圖6-22中,在第 k 層中讀塊12中的一個數據對象,會導致一個緩存不命中,因為塊12當前不在第 k 層緩存中。一旦把塊12從第 k 十1層復制到第 k 層之後,它就會保持在那裡,等待稍後的訪問。
3. 緩存不命中的種類
區分不同種類的緩存不命中有時候是很有幫助的。如果第 k 層的緩存是空的,那麼對
任何數據對象的訪問都會不命中。一個空的緩存有時被稱為冷緩存( cold cache ),此類不命中稱為強制性不命中( compulsory miss )或冷不命中( cold miss )。冷不命中很重要,因為它們通常是短暫的事件,不會在反復訪問存儲器使得緩存暖身( warmed up )之後的穩定狀態中出現。
只要發生了不命中,第 k 層的緩存就必須執行某個放置策略( placement policy ),確定把它從第 k 十1層中取出的塊放在哪裡。最靈活的替換策略是允許來自第 k +1層的任何塊放在第 k 層的任何塊中。對於存儲器層次結構中高層的緩存(靠近 CPU ),它們是用硬體來實現的,而且速度是最優的,這個策略實現起來通常很昂貴,因為隨機地放置塊,定位起來代價很高。
因此,硬體緩存通常使用的是更嚴格的放置策略,這個策略將第 k 十1層的某個塊限制放置在第 k 層塊的一個小的子集中(有時只是一個塊)。例如,在圖6-22中,我們可以確定第 k 十1層的塊 i 必須放置在第 k 層的塊( i mod 4)中。例如,第 k 十1層的塊0、4、8和12會映射到第 k 層的塊0;塊1、5、9和13會映射到塊1;依此類推。注意,圖6-22中的示例緩存使用的就是這個策略。
這種限制性的放置策略會引起一種不命中,稱為沖突不命中( conflict miss ),在這種情況中,緩存足夠大,能夠保存被引用的數據對象,但是因為這些對象會映射到同一個緩存塊,緩存會一直不命中。例如,在圖6-22中,如果程序請求塊0,然後塊8,然後塊0,然後塊8,依此類推,在第 k 層的緩存中,對這兩個塊的每次引用都會不命中,即使這個緩存總共可以容納4個塊。
程序通常是按照一系列階段(如循環)來運行的,每個階段訪問緩存塊的某個相對穩定不變的集合。例如,一個嵌套循環可能會反復地訪問同一個數組的元素。這個塊的集合稱為這個階段的工作集( working set )。當工作集的大小超過緩存的大小時,緩存會經歷容量不命中( capacity miss )。換句話說就是,緩存太小了,不能處理這個工作集。
4. 緩存管理
正如我們提到過的,存儲器層次結構的本質是,每一層存儲設備都是較低一層的緩存。在每一層上,某種形式的邏輯必須管理緩存。這里,我們的意思是指某個東西要將緩存劃分成塊,在不同的層之間傳送塊,判定是命中還是不命中,並處理它們。管理緩存的邏輯可以是硬體、軟體,或是兩者的結合。
例如,編譯器管理寄存器文件,緩存層次結構的最高層。它決定當發生不命中時何時發射載入,以及確定哪個寄存器來存放數據。L1、L2和L3層的緩存完全是由內置在緩存中的硬體邏輯來管理的。在一個有虛擬內存的系統中, DRAM 主存作為存儲在磁碟上的數據塊的緩存,是由操作系統軟體和 CPU 上的地址翻譯硬體共同管理的。對於一個具有像 AFS 這樣的分布式文件系統的機器來說,本地磁碟作為緩存,它是由運行在本地機器上的 AFS 客戶端進程管理的。在大多數時候,緩存都是自動運行的,不需要程序採取特殊的或顯式的行動。
6.3.2 存儲器層次結構概念小結
概括來說,基於緩存的存儲器層次結構行之有效,是因為較慢的存儲設備比較快的存儲設備更便宜,還因為程序傾向於展示局部性:
1)利用時間局部性: 由於時間局部性,同一數據對象可能會被多次使用。一旦一個數據對象在第一次不命中時被復制到緩存中,我們就會期望後面對該目標有一系列的訪問命中。因為緩存比低一層的存儲設備更快,對後面的命中的服務會比最開始的不命中快很多。
2)利用空間局部性: 塊通常包含有多個數據對象。由於空間局部性,我們會期望後面對該塊中其他對象的訪問能夠補償不命中後復制該塊的花費。現代系統中到處都使用了緩存。正如從圖6-23中能夠看到的那樣, CPU 晶元、操作系統、分布式文件系統中和萬維網上都使用了緩存。各種各樣硬體和軟體的組合構成和管理著緩存。注意,圖6-23中有大量我們還未涉及的術語和縮寫。在此我們包括這些術語和縮寫是為了說明緩存是多麼的普遍。
B. 「Keil C51」下如何讓編譯器優先使用片內「RAM」
C51內存結構深度剖析
在編寫應用程序時,定義一個變數,一個數組,或是說一個固定表格,到底存儲在什麼地方;當定義變數大小超過MCU的內存范圍時怎麼辦;如何控制變數定義不超過存儲范圍;以及如何定義變數才能使得變數訪問速度最快,寫出的程序運行效率最高。以下將一一解答。
1 六類關鍵字(六類存儲類型)
data idata xdata pdata code bdata
code: code memory (程序存儲器也即只讀存儲器)用來保存常量或是程序。code memory 採用16位地址線編碼,可以是在片內,或是片外,大小被限制在64KB
作用:定義常量,如八段數碼表或是編程使用的常,在定義時加上code 或明確指明定義的常量保存到code memory(只讀)
使用方法:
char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
此關鍵字的使用方法等同於const
data data memory (數據存儲區)只能用於聲明變數,不能用來聲明函數,該區域位於片內,採用8位地址線編碼,具有最快的存儲速度,但是數量被限制在128byte或更少。
使用方法:
unsigned char data fast_variable=0;
idata idata memory(數據存儲區)只能用於聲明變數,不能用來聲明函數. 該區域位於片內,採用8位地址線編碼,內存大小被限制在256byte或更少。該區域的低地址區與data memory地址一致;高地址區域是52系列在51系列基礎上擴展的並與特殊功能寄存器具有相同地址編碼的區域。即:data memory是idata memory的一個子集。
xdata xdata memory 只能用於聲明變數,不能用來聲明函數,該區域位於MCU
外部,採用16位地址線進行編碼,存儲大小被限制在64KB以內。
使用方法:
unsigned char xdata count=0;
pdata pdata memory 只能用於聲明變數,不能用來聲明函數,該區域位於MCU外部,採用8位地址線進行編碼。存儲大小限制在256byte. 是xdata memory的低256byte。為其子集。
使用方法
unsigned char pdata count=0;
bdata bdata memory 只能用於聲明變數,不能用來聲明函數。該區域位於8051內部位數據地址。定義的量保存在內部位地址空間,可用位指令直接讀寫。
使用方法:
unsigned char bdata varab=0
註:有些資料講,定義字元型變數時,在預設unsigned 時,字元型變數,默認為無符號,與標准C不同,但我在Keil uVision3中測試的時候發現並非如此。在預設的情況下默認為有符號。或許在以前的編譯器是默認為無符號。所以看到有的資料上面這樣講的時候,要注意一下,不同的編譯器或許不同。所以我們在寫程序的時候,還是乖乖的把unsigned signed 加上,咱也別偷這個懶。
2函數的參數和局部變數的存儲模式
C51 編譯器允許採用三種存儲器模式:SMALL,COMPACT 和LARGE。一個函數的存儲器模式確定了函數的參數的局部變數在內存中的地址空間。處於SMALL模式下的函數參數和局部變數位於8051單片機內部RAM中,處於COMPACT和LARGE模式下的函數參數和局部變數則使用單片機外部RAM。在定義一個函數時可以明確指定該函數的存儲器模式。方法是在形參表列的後面加上一存儲模式。
示例如下:
#pragma large //此預編譯必須放在所有頭文前面
int func0(char x,y) small;
char func1(int x) large;
int func2(char x);
註:
上面例子在第一行用了一個預編譯命令#pragma 它的意思是告訴c51編譯器在對程序進行編譯時,按該預編譯命令後面給出的編譯控制指令LARGE進行編譯,即本常式序編譯時的默認存儲模式為LARGE.隨後定義了三個函數,第一個定義為SMALL存儲模式,第二個函數定義為LARGE第三個函數未指定,在用C51進行編譯時,只有最後一個函數按LARGE存儲器模式處理,其它則分別按它們各自指定的存儲器模式處理。
本例說明,C51編譯器允許採用所謂的存儲器混合模式,即允許在一個程序中將一些函數使用一種存儲模式,而其它一些則按另一種存儲器模式,採用存儲器混合模式編程,可以充分利用8051系列單片機中有限的存儲器空間,同時還可以加快程序的執行速度。
3絕對地址訪問 absacc.h(相當重要)
#define CBYTE ((unsigned char volatile code *) 0)
#define DBYTE ((unsigned char volatile data *) 0)
#define PBYTE ((unsigned char volatile pdata *) 0)
#define XBYTE ((unsigned char volatile xdata *) 0)
功能:CBYTE 定址 CODE區
DBYTE 定址 DATA區
PBYTE 定址 XDATA(低256)區
XBYTE 定址 XDATA區
例: 如下指令在對外部存儲器區域訪問地址0x1000
xvar=XBYTE[0x1000];
XBYTE[0x1000]=20;
#define CWORD ((unsigned int volatile code *) 0)
#define DWORD ((unsigned int volatile data *) 0)
#define PWORD ((unsigned int volatile pdata *) 0)
#define XWORD ((unsigned int volatile xdata *) 0)
功能:與前面的一個宏相似,只是它們指定的數據類型為unsigned int .。
通過靈活運用不同的數據類型,所有的8051地址空間都是可以進行訪問。
如
DWORD[0x0004]=0x12F8;
即內部數據存儲器中(0x08)=0x12; (0x09)=0xF8
註:用以上八個函數,可以完成對單片機內部任意ROM和RAM進行訪問,非常方便。還有一種方法,那就是用指鍾,後面會對C51的指針有詳細的介紹。
4寄存器變數(register)
為了提高程序的執行效率,C語言允許將一些頻率最高的那些變數,定義為能夠直接使用硬體寄存器的所謂的寄存器變數。定義一個變數時,在變數類型名前冠以「register」 即將該變數定義成為了寄存器變數。寄存器變數可以認為是一自動變數的一種。有效作用范圍也自動變數相同。由於計算機寄存器中寄存器是有限的。不能將所有變數都定義成為寄存器變數,通常在程序中定義寄存器變數時,只是給編譯器一個建議,該變數是否真正成為寄存器變數,要由編譯器根據實際情況來確定。另一方面,C51編譯器能夠識別程序中使用頻率最高的變數,在可能的情況下,即使程序中並未將該變數定義為寄存器變數,編譯器也會自動將其作為寄存器變數處理。被定義的變數是否真正能成為寄存器變數,最終是由編譯器決定的。
5內存訪問雜談
1指鍾
指鍾本身是一個變數,其中存放的內容是變數的地址,也即特定的數據。8051的地址是16位的,所以指針變數本身佔用兩個存儲單元。指針的說明與變數的說明類似,僅在指針名前加上「*」即可。
如 int *int_point; 聲明一個整型指針
char *char_point; 聲明一個字元型指針
利用指針可以間接存取變數。實現這一點要用到兩個特殊運算符
& 取變數地址
* 取指針指向單元的數據
示例一:
int a,b;
int *int_point; //定義一個指向整型變數的指針
a=15;
int_point=&a; //int_point指向 a
*int_point=5; //給int_point指向的變數a 賦值5 等同於a=5;
示例二:
char i,table[6],*char_point;
char_point=table;
for(i=0;i<6;i++)
{
char_point=i;
char_point++;
}
註:
指針可以進行運算,它可以與整數進行加減運算(移動指針)。但要注意,移動指針後,其地址的增減量是隨指針類型而異的,如,浮點指針進行自增後,其內部將在原有的基礎上加4,而字元指針當進生自增的時候,其內容將加1。原因是浮點數,佔4個內存單元,而字元佔一個位元組。
宏晶科技最新一代STC12C5A360S2系列,每一個單片機出廠時都有全球唯一身份證號碼(ID號),用戶可以在單片機上電後讀取內部RAM單元F1H~F7H的數值,來獲取此單片機的唯一身份證號碼。使用MOV @Ri 指令來讀取。下面介紹C51 獲取方法:
char id[7]={0};
char i;
char idata *point;
for(i=0;i<7;i++)
{
id[i]=*point;
point++;
}
(此處只是對指針做一個小的介紹,達到訪問內部任何空間的方式,後述有對指針使用的詳細介紹)
2對SFR,RAM ,ROM的直接存取
C51提供了一組可以直接對其操作的擴展函數
若源程序中,用#include包含頭文件,io51.h 後,就可以在擴展函數中使用特殊功能寄存器的地址名,以增強程序的可讀性:
注 此方法對SFR,RAM,ROM的直接存取不建議使用.因為,淡io51.h這個頭文件在KEIL中無法打開,可用指針,或是採用absacc.h頭文件,
C. as400的存儲結構
AS/400 又一特徵就是它的存儲管理方法。它採用了單級存儲( Single-level store )結構對存儲器進行管理。它將系統中所有的存儲器,即所有主存 (main storage ) 和輔存 (Auxiliary storage ) 看成是一個大的存儲器,這個大存儲器提供的存儲空間相當於機器的定址能力,從開始的 48 位定址空間到今天的 64 位定址空間,近幾年內渴望實現 128 位定址空間。這個諾大的虛地址空間給用戶尤其是程序員帶來了極大的方便,他們所面對的就是一個盛裝對象( Objects )的大容器,只要知道了一個對象的名稱和類型就可以訪問這個對象,完全不必關心所訪問的對象存放在哪個盤上,也不必考慮存儲管理等繁瑣問題。至於內外存之間信息如何交換以及為提高對象的訪問效率而將一個對象的內容分散存放到幾個硬碟上,對用戶是完全透明的。
事實上,在 MI 上方的任何程序(包括 OS/400 ,應用程序,編譯器等)都看不到這個單級存儲結構中的實存部分(內存和外存),呈現在他們面前的只是一個偌大的虛存, 每個 AS/400 對象佔用這個虛存中的一些虛地址。當用戶使用對象的名字和類型訪問它時, MI 層上方的程序可以快速地將這個對象的名字映射成它的虛地址。由於 MI 層上方用戶編程時,不再區分內存和外存,只是針對這個虛存編寫程序,因此可使得編寫程序所需的指令數目大大減少。當然,負責系統性能管理的人員用 WRKSYSSTS 命令或 WRKDSKSTS 命令還是可以看到一些關於磁碟的信息的,例如磁碟的使用率,忙閑情況,缺頁率( page Fault )等。
對象總是要存儲在磁碟上,並且在使用時需將其裝入內存的。如何將對象的虛地址映射到磁碟上的實地址或內存的實地址是在 MI 層下方實現的。 AS/400 將內存分為大小相等的一些頁面幀 (page frame) ,在 64 位 AS/400 系統中,頁面幀的大小為 4096 位元組( 4KB ),存儲在磁碟上的對象被劃分為一個個大小與頁面幀相等的頁面( page )。一個大的對象由許多頁面組成,這些頁面在虛存中佔用一些連續的地址,但在實際存放時,可能被分散到幾個硬碟上,這樣做可以提高對對象的讀寫速度。當處理機要訪問的一個對象時,可以將該對象的部分頁面從磁碟裝入內存的頁面幀中, MI 下方的地址轉換機構通過查詢駐留在內存中的一個頁表 (page table) 實現虛實地址轉換,如果發現一個虛地址對應的頁面不在內存中,就報告頁故障 (page fault) ,然後由內存管理程序負責將這個頁面從磁碟調入內存。硬體在查詢頁表時使用的是 Hashing 演算法。
總之, AS/400 的單級存儲結構不僅給編程用戶提供了一個相當大的存儲空間,同時也將用戶從繁瑣的存儲管理中解放出來,減輕了用戶負擔,提高了工作效率。 在 OS/400 V4R3 及其以後版本中,已把 AS/400 的單級存儲的單一地址空間由原來的內存、硬碟推廣到磁帶庫,系統的存儲管理功能可按用戶自定義的規則,自動透明地管理分布在磁碟與磁帶庫中的數據。