為什麼要記憶體對齊 Data alignment: Straighten up and fly right
為了速度和正確性,請對齊你的資料.
概述:對於所有直接操作記憶體的程式設計師來說,資料對齊都是很重要的問題.資料對齊對你的程式的表現甚至能否正常執行都會產生影響.就像本文章闡述的一樣,理解了對齊的本質還能夠解釋一些處理器的"奇怪的"行為.
記憶體存取粒度
程式設計師通常傾向於認為記憶體就像一個位元組陣列.在C及其衍生語言中,char * 用來指代"一塊記憶體",甚至在JAVA中也有byte[]型別來指代實體記憶體.
Figure 1. 程式設計師是如何看記憶體的
然而,你的處理器並不是按位元組塊來存取記憶體的.它一般會以雙位元組,四位元組,8位元組,16位元組甚至
Figure 2. 處理器是如何看記憶體的
高層(語言)程式設計師認為的記憶體形態和處理器對記憶體的實際處理方式之間的差異產生了許多有趣的問題,本文旨在闡述這些問題.
如果你不理解記憶體對齊,你編寫的程式將有可能產生下面的問題,按嚴重程度遞增:
- 程式執行速度變慢
- 應用程式產生死鎖
- 作業系統崩潰
- 你的程式會毫無徵兆的出錯,產生錯誤的結果(silently fail如何翻譯?)
記憶體對齊基礎
為了說明記憶體對齊背後的原理,我們考察一個任務,並觀察記憶體存取粒度是如何對該任務產生影響的.這個任務很簡單:先從地址0讀取4個位元組到暫存器
首先考察記憶體存取粒度為1byte的情況:
Figure 3. 單位元組存取
這迎合了那些天真的程式設計師的觀點:從地址0和地址1讀取4位元組資料都需要相同的4次操作.現在再看看存取粒度為雙位元組的處理器(像最初的68000處理器)的情況:
Figure 4. 雙位元組存取
從地址0讀取資料,雙位元組存取粒度的處理器讀記憶體的次數是單位元組存取粒度處理器的一半.因為每次記憶體存取都會產生一個固定的開銷,最小化記憶體存取次數將提升程式的效能.
但從地址1讀取資料時由於地址1沒有和處理器的記憶體存取邊界對齊,處理器就會做一些額外的工作.地址1這樣的地址被稱作非對齊地址
最後我們再看一下存取粒度為4位元組的處理器(像68030,PowerPC® 601)的情況:
Figure 5. 四位元組存取
在對齊的記憶體地址上,四位元組存取粒度處理器可以一次性的將4個位元組全部讀出;而在非對齊的記憶體地址上,讀取次數將加倍.
既然你理解了記憶體對齊背後的原理,那麼你就可以探索該領域相關的一些問題了.
懶惰的處理器
處理器對非對齊記憶體的存取有一些技巧.考慮上面的四位元組存取粒度處理器從地址1讀取4位元組的情況,你肯定想到了下面的解決方法:
Figure 6. 處理器如何處理非對齊記憶體地址
處理器先從非對齊地址讀取第一個4位元組塊,剔除不想要的位元組,然後讀取下一個4位元組塊,同樣剔除不要的資料,最後留下的兩塊資料合併放入暫存器.這需要做很多工作.
有些處理器並不情願為你做這些工作.
最初的68000處理器的存取粒度是雙位元組,沒有應對非對齊記憶體地址的電路系統.當遇到非對齊記憶體地址的存取時,它將丟擲一個異常.最初的Mac OS並沒有妥善處理這個異常,它會直接要求使用者重啟機器.悲劇.
隨後的680x0系列,像68020,放寬了這個的限制,支援了非對齊記憶體地址存取的相關操作.這解釋了為什麼一些在68020上正常執行的舊軟體會在68000上崩潰.這也解釋了為什麼當時一些老Mac程式設計人員會將指標初始化成奇數地址.在最初的Mac機器上如果指標在使用前沒有被重新賦值成有效地址,Mac會立即跳到偵錯程式.通常他們通過檢查呼叫堆疊會找到問題所在.
所有的處理器都使用有限的電晶體來完成工作.支援非對齊記憶體地址的存取操作會消減"電晶體預算",這些電晶體原本可以用來提升其他模組的速度或者增加新的功能.
以速度的名義犧牲非對齊記憶體存取功能的一個例子就是MIPS.為了提升速度,MIPS幾乎廢除了所有的瑣碎功能.
PowerPC各取所長.目前所有的PowPC都硬體支援非對齊的32位整型的存取.雖然犧牲掉了一部分效能,但這些損失在逐漸減少.
另一方面,現今的PowPC處理器缺少對非對齊的64-bit浮點型資料的存取的硬體支援.當被要求從非對齊記憶體讀取浮點數時,PowerPC會丟擲異常並讓作業系統來處理記憶體對齊這樣的雜事.軟體解決記憶體對齊要比硬體慢得多.
速度
下面編寫一些測試來說明非對齊記憶體對效能造成的損失.過程很簡單:從一個10MB的緩衝區中讀取,取反,並寫回資料.這些測試有兩個變數:
- 處理緩衝區的處理粒度,單位bytes.一開始每次處理1個位元組,然後2個位元組,4個位元組和8個位元組.
- 緩衝區的對準.用每次增加緩衝區的指標來交錯調整記憶體地址,然後重新做每個測試.
這些測試執行在800MHz的PowerBook G4上.為了最小化中斷引起的波動,這裡取十次結果的平均值.第一個是處理粒度為單位元組的情況:
Listing 1. 每次處理一個位元組
void Munge8( void *data, uint32_t size ){
uint8_t *data8 = (uint8_t*)data;
uint8_t *data8End = data8 +size;
while( data8 != data8End ){
*data8++ = -*data8;
}
}
執行這個函式需要67364微秒,現在修改成每次處理2個位元組,這將使存取次數減半:
Listing 2.每次處理2個位元組
void Munge16( void *data, uint32_t size ){
uint16_t *data16 = (uint16_t*)data;
uint16_t *data16End = data16 + (size>> 1); /* Divide size by 2. */
uint8_t *data8 = (uint8_t*)data16End;
uint8_t *data8End = data8 + (size& 0x00000001); /* Strip upper 31 bits. */
while( data16 != data16End ){
*data16++ = -*data16;
}
while( data8 != data8End ){
*data8++ = -*data8;
}
}
如果處理的記憶體地址是對齊的話,上述函式處理同一個緩衝區需要48765微秒--比Munge8快38%.如果緩衝區不是對齊的,處理時間會增加到66385微秒--比對齊情況下慢了27%.下圖展示了對齊記憶體和非對齊記憶體之間的效能對比.
Figure7. 單位元組存取 vs.雙位元組存取
第一個讓人注意到的現象是單位元組存取結果很均勻,且都很慢.第二個是雙位元組存取時,每當地址是單數時,變慢的27%就會出現.
下面加大賭注,每次處理4個位元組:
Listing 3. 每次處理4個位元組
void Munge32( void *data, uint32_t size ){
uint32_t *data32 = (uint32_t*)data;
uint32_t *data32End = data32 + (size>> 2); /* Divide size by 4. */
uint8_t *data8 = (uint8_t*)data32End;
uint8_t *data8End = data8 + (size& 0x00000003); /* Strip upper 30 bits. */
while( data32 != data32End ){
*data32++ = -*data32;
}
while( data8 != data8End ){
*data8++ = -*data8;
}
}
對於對齊的緩衝區,函式需要43043微秒;對於非對齊的緩衝區,函式需要55775微秒.因此,在所測試的機器上,非對齊地址的四位元組存取速度比對齊地址的雙位元組存取速度要慢.
Figure8. 單位元組vs.雙位元組vs.四位元組存取
現在來最恐怖的:每次處理8個位元組:
Listing 4.每次處理8個位元組
void Munge64( void *data, uint32_t size ){
double *data64 = (double*)data;
double *data64End = data64 + (size>> 3); /* Divide size by 8. */
uint8_t *data8 = (uint8_t*)data64End;
uint8_t *data8End = data8 + (size& 0x00000007); /* Strip upper 29 bits. */
while( data64 != data64End ){
*data64++ = -*data64;
}
while( data8 != data8End ){
*data8++ = -*data8;
}
}
Munge64處理對齊的緩衝區需要39085微秒--大約比對齊的Munge32快10%.但是,在非對齊緩衝區上的處理時間是讓人驚訝的1841155微秒--比對齊的慢了兩個數量級,慢了足足4610%.
怎麼回事?因為我們現今所使用的PowerPC缺少對存取非對齊記憶體的浮點數的硬體支援.對每次非對齊記憶體的存取,處理器都丟擲一個異常.作業系統獲取該異常並軟體實現記憶體對齊.下圖顯示了非對齊記憶體存取帶來的不利後果.
Figure 9. 多位元組存取對比
單位元組,雙位元組和四位元組的細節都被掩蓋了.或許去除頂部以後的圖形,如下圖,更清晰:
Figure 10. 多位元組存取對比 #2
在這些資料背後還隱藏著一個微妙的現象.比較8位元組粒度時邊界是4的倍數的記憶體的存取速度:
Figure10. 多位元組存取對比 #3
你會發現8位元組粒度時邊界為4和12位元組的記憶體存取速度要比相同情況下的4和2位元組粒度的慢.即使PowerPC硬體支援4位元組對齊的8位元組雙浮點型資料的存取,你還是要承擔額外的開銷造成的損失.誠然,這種損失絕不會像4610%那麼大,但還是不能忽略的.這個實驗告訴我們:存取非對齊記憶體時,大粒度的存取可能會比小粒度存取還要慢.
The End.
關於文章:文章並沒有翻譯完,還有一點關於原子性和Altivec的內容,以我現在的水平還研究不了那些東西.有興趣自己看.
關於翻譯:最大的難點在於penalty該如何翻譯,文中我都繞開了,希望指教.
關於內容:如果想了解實際的結構體的記憶體對齊,請參考我的博文:點選開啟連結
龍西村原創,嚴禁用於商業用途,轉載請註明出處。