非对齐内存访问
=========================
非對(duì)齊內(nèi)存訪問(wèn)
:作者: Daniel Drake dsd@gentoo.org,
:作者: Johannes Berg johannes@sipsolutions.net
:以及和來(lái)自他們的幫助: Alan Cox, Avuton Olrich, Heikki Orsila, Jan Engelhardt,
Kyle McMartin, Kyle Moffett, Randy Dunlap, Robert Hancock, Uli Kunitz,
Vadim Lobanov
Linux 運(yùn)行在各種架構(gòu)上,這些架構(gòu)在內(nèi)存訪問(wèn)方面具有不同的行為。這篇文檔描述了一些關(guān)于非對(duì)齊訪問(wèn)的細(xì)節(jié),本文檔提供了有關(guān)未對(duì)齊訪問(wèn)的一些詳細(xì)信息,為什么需要編寫(xiě)不會(huì)導(dǎo)致它們的代碼,以及如何編寫(xiě)此類代碼!
非對(duì)齊訪問(wèn)的定義
當(dāng)你嘗試從不能被 N 整除的地址(即 addr % N != 0)開(kāi)始讀取 N 個(gè)字節(jié)的數(shù)據(jù)時(shí),會(huì)發(fā)生未對(duì)齊的內(nèi)存訪問(wèn)。例如,從地址0x10004處讀取4個(gè)字節(jié)的數(shù)據(jù)是不錯(cuò)的,但是從地址0x10005處讀取4個(gè)字節(jié)就會(huì)導(dǎo)致非對(duì)齊內(nèi)存訪問(wèn)。
以上看起來(lái)可能有些不清楚,因?yàn)閮?nèi)存訪問(wèn)可以以不同的方式發(fā)生。這里的上下文是在機(jī)器代碼別:某些指令從內(nèi)存讀取或?qū)懭攵鄠€(gè)字節(jié)(例如 x86 匯編中的 movb、movw、movl)。很明顯,識(shí)別那些會(huì)被編譯為多字節(jié)內(nèi)存訪問(wèn)指令的 C 語(yǔ)句相對(duì)容易,即在處理 u16、u32 和 u64 等類型時(shí)。
自然對(duì)齊
上面提到的規(guī)則形成了我們所說(shuō)的自然對(duì)齊:
訪問(wèn) N 字節(jié)內(nèi)存時(shí),內(nèi)存基地址必須能被 N 整除,即 addr % N == 0。
在編寫(xiě)代碼時(shí),需要假設(shè)目標(biāo)架構(gòu)有自然的對(duì)齊要求。
實(shí)際上,只有少數(shù)架構(gòu)需要對(duì)所有大小的內(nèi)存訪問(wèn)進(jìn)行自然對(duì)齊。 但是,我們必須考慮所有支持的架構(gòu); 編寫(xiě)滿足自然對(duì)齊要求的代碼是實(shí)現(xiàn)完全可移植性的最簡(jiǎn)單方法。
為什么非對(duì)齊訪問(wèn)不好
執(zhí)行未對(duì)齊內(nèi)存訪問(wèn)的效果因架構(gòu)而異。 在這里寫(xiě)一篇關(guān)于差異的完整文檔會(huì)很容易;常見(jiàn)情況概述如下:
- Some architectures are able to perform unaligned memory accesses
transparently, but there is usually a significant performance cost. - 某些體系結(jié)構(gòu)能夠透明地執(zhí)行未對(duì)齊的內(nèi)存訪問(wèn),但通常會(huì)產(chǎn)生顯著的性能成本。
- 當(dāng)發(fā)生未對(duì)齊的訪問(wèn)時(shí),某些架構(gòu)會(huì)引發(fā)處理器異常。異常處理程序能夠糾正未對(duì)齊的訪問(wèn),但對(duì)性能的影響很大。
- 某些架構(gòu)在發(fā)生未對(duì)齊訪問(wèn)時(shí)會(huì)引發(fā)處理器異常,但這些異常沒(méi)有包含足夠的信息來(lái)糾正未對(duì)齊訪問(wèn)。
- 某些架構(gòu)無(wú)法進(jìn)行未對(duì)齊的內(nèi)存訪問(wèn),但會(huì)默默地對(duì)請(qǐng)求的內(nèi)存執(zhí)行不同的內(nèi)存訪問(wèn),從而導(dǎo)致難以檢測(cè)的微妙的代碼錯(cuò)誤!
從上面應(yīng)該可以明顯看出,如果你的代碼導(dǎo)致發(fā)生未對(duì)齊的內(nèi)存訪問(wèn),您的代碼將無(wú)法在某些平臺(tái)上正常工作,并且會(huì)在其他平臺(tái)上導(dǎo)致性能問(wèn)題。
不會(huì)引起非對(duì)齊訪問(wèn)的代碼
乍一看,上面的概念似乎與實(shí)際的編碼實(shí)踐聯(lián)系起來(lái)有點(diǎn)困難。 畢竟,您對(duì)某些變量的內(nèi)存地址等沒(méi)有太多控制權(quán)。
幸運(yùn)的是那并不是很復(fù)雜,就像在大多數(shù)場(chǎng)景下,編譯器會(huì)確保這些對(duì)你有用。例如,采用以下結(jié)構(gòu):
struct foo {u16 field1;u32 field2;u8 field3; };假設(shè)有一個(gè)上面結(jié)構(gòu)的實(shí)例駐留在以0x10000開(kāi)始的地址。一個(gè)基本的理解是,訪問(wèn)filed2會(huì)導(dǎo)致非對(duì)齊訪問(wèn)是正常的。你期望 field2 位于結(jié)構(gòu)中偏移 2 個(gè)字節(jié)處,即地址 0x10002,但該地址不能被 4 整除(請(qǐng)記住,我們?cè)谶@里讀取的是 4 個(gè)字節(jié)的值)。
幸運(yùn)的是,編譯器理解對(duì)齊限制,所以在上面的場(chǎng)景中它會(huì)在field1和field2之間插入兩個(gè)字節(jié)。因此,對(duì)于標(biāo)準(zhǔn)結(jié)構(gòu)類型,你始終可以依靠編譯器來(lái)填充結(jié)構(gòu),以便對(duì)字段的訪問(wèn)適當(dāng)對(duì)齊(假設(shè)您沒(méi)有將字段強(qiáng)制轉(zhuǎn)換為不同長(zhǎng)度的類型)。
同樣,你也可以依靠編譯器根據(jù)變量類型的大小將變量和函數(shù)參數(shù)對(duì)齊到自然對(duì)齊的方案。
在這點(diǎn)上,不言而喻,訪問(wèn)單個(gè)字節(jié)(u8或者char)永遠(yuǎn)不會(huì)造成非對(duì)齊,因?yàn)樗械膬?nèi)存地址是可以被1整除的。
在一個(gè)相關(guān)主題上,考慮到上述注意事項(xiàng),你可以觀察到,你可以對(duì)結(jié)構(gòu)中的字段重新排序,以便將字段放置會(huì)插入填充的位置,從而減少結(jié)構(gòu)實(shí)例的整體駐留內(nèi)存大小。 的最佳布局
上面的例子是::
對(duì)于自然對(duì)齊方案,編譯器只需在結(jié)構(gòu)末尾添加一個(gè)字節(jié)的填充。添加此填充是為了滿足這些結(jié)構(gòu)的數(shù)組的對(duì)齊約束。
另一個(gè)值得考慮的點(diǎn)是在一個(gè)結(jié)構(gòu)類型中__ attribute__((packed))的使用。這個(gè)特定于 GCC 的屬性告訴編譯器永遠(yuǎn)不要在結(jié)構(gòu)中插入任何填充,當(dāng)您想使用 C 結(jié)構(gòu)來(lái)表示一些固定順序排列的數(shù)據(jù)時(shí)非常有用。
您可能會(huì)認(rèn)為,在訪問(wèn)不滿足體系結(jié)構(gòu)對(duì)齊要求的字段時(shí),使用此屬性很容易導(dǎo)致未對(duì)齊的訪問(wèn)。 然而,同樣,編譯器知道對(duì)齊約束,并將生成額外的指令來(lái)以不會(huì)導(dǎo)致未對(duì)齊訪問(wèn)的方式執(zhí)行內(nèi)存訪問(wèn)。 當(dāng)然,與非壓縮情況相比,額外的指令顯然會(huì)導(dǎo)致性能損失,因此只有在避免結(jié)構(gòu)填充很重要時(shí)才應(yīng)使用壓縮屬性。
會(huì)造成非對(duì)齊訪問(wèn)的代碼
考慮到上述情況,讓我們進(jìn)入一個(gè)可能導(dǎo)致未對(duì)齊內(nèi)存訪問(wèn)的函數(shù)的真實(shí)示例。 以下來(lái)自 include/linux/etherdevice.h 的函數(shù)是一個(gè)優(yōu)化的例程,用于比較兩個(gè)以太網(wǎng) MAC 地址的相等性:
bool ether_addr_equal(const u8 *addr1, const u8 *addr2){#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESSu32 fold = ((*(const u32 *)addr1) ^ (*(const u32 *)addr2)) |((*(const u16 *)(addr1 + 4)) ^ (*(const u16 *)(addr2 + 4)));return fold == 0;#elseconst u16 *a = (const u16 *)addr1;const u16 *b = (const u16 *)addr2;return ((a[0] ^ b[0]) | (a[1] ^ b[1]) | (a[2] ^ b[2])) == 0;#endif}在上面的函數(shù)中,當(dāng)硬件具有高效的非對(duì)齊訪問(wèn)能力時(shí),這段代碼沒(méi)有問(wèn)題。 但是當(dāng)硬件無(wú)法訪問(wèn)任意邊界上的內(nèi)存時(shí),對(duì) a[0] 的引用會(huì)導(dǎo)致從地址 addr1 開(kāi)始的內(nèi)存中讀取 2 個(gè)字節(jié)(16 位)。
Think about what would happen if addr1 was an odd address such as 0x10003.
(Hint: it’d be an unaligned access.)
想想如果addr1是一個(gè)奇數(shù)地址比如0x10003,會(huì)發(fā)生什么(提示:這是一個(gè)非對(duì)齊訪問(wèn))。
盡管上述函數(shù)存在潛在的未對(duì)齊訪問(wèn)問(wèn)題,但無(wú)論如何它都包含在內(nèi)核中,但被理解為只能在 16 位對(duì)齊地址上正常工作。 由調(diào)用者來(lái)確保這種對(duì)齊或根本不使用此功能。 這種對(duì)齊不安全的功能仍然很有用,因?yàn)樗鼘?duì)于可以確保對(duì)齊的情況來(lái)說(shuō)是一個(gè)不錯(cuò)的優(yōu)化,在以太網(wǎng)網(wǎng)絡(luò)環(huán)境中幾乎所有場(chǎng)景都是如此。
下面是一些可能導(dǎo)致未對(duì)齊訪問(wèn)的代碼示例:
void myfunc(u8 *data, u32 value) {[...]*((u32 *) data) = cpu_to_le32(value);[...] }每當(dāng) data 參數(shù)指向一個(gè)不能被 4 整除的地址時(shí),此代碼將導(dǎo)致未對(duì)齊的訪問(wèn)。
總之,您可能會(huì)遇到未對(duì)齊訪問(wèn)問(wèn)題的 2 個(gè)主要場(chǎng)景包括:
避免非對(duì)齊訪問(wèn)
避免未對(duì)齊訪問(wèn)的最簡(jiǎn)單方法是使用 <asm/unaligned.h> 頭文件提供的 get_unaligned() 和 put_unaligned() 宏。
回到之前可能導(dǎo)致未對(duì)齊訪問(wèn)的代碼示例:
void myfunc(u8 *data, u32 value) {[...]*((u32 *) data) = cpu_to_le32(value);[...] }為了避免非對(duì)齊內(nèi)存訪問(wèn),你可以像下面一樣重寫(xiě):
void myfunc(u8 *data, u32 value) {[...]value = cpu_to_le32(value);put_unaligned(value, (u32 *) data);[...] }get_unaligned() 宏的工作原理類似。 假設(shè) ‘data’ 是一個(gè)指向內(nèi)存的指針并且您希望避免未對(duì)齊的訪問(wèn),它的用法如下:
u32 value = get_unaligned((u32 *) data);這些宏適用于任何長(zhǎng)度的內(nèi)存訪問(wèn)(不僅僅是上面例子中的 32 位)。請(qǐng)注意,與對(duì)齊內(nèi)存的標(biāo)準(zhǔn)訪問(wèn)相比,使用這些宏訪問(wèn)未對(duì)齊的內(nèi)存在性能方面可能代價(jià)高昂。
如果使用此類宏不方便,另一種選擇是使用 memcpy(),其中源或目標(biāo)(或兩者)的類型為 u8* 或 unsigned char*。由于此操作的逐字節(jié)性質(zhì),避免了未對(duì)齊的訪問(wèn)。
對(duì)齊與網(wǎng)絡(luò)
在需要對(duì)齊負(fù)載的架構(gòu)上,網(wǎng)絡(luò)要求 IP 標(biāo)頭在四字節(jié)邊界上對(duì)齊以優(yōu)化 IP 堆棧。 對(duì)于常規(guī)以太網(wǎng)硬件,使用常量 NET_IP_ALIGN。 在大多數(shù)架構(gòu)上,這個(gè)常量的值為 2,因?yàn)檎5囊蕴W(wǎng)頭是 14 字節(jié)長(zhǎng),所以為了獲得正確的對(duì)齊,需要 DMA 到一個(gè)可以表示為 4*n + 2 的地址。這里一個(gè)值得注意的例外是 powerpc 它將 NET_IP_ALIGN 定義為 0,因?yàn)槲磳?duì)齊地址的 DMA 可能非常昂貴,并且使未對(duì)齊加載的成本相形見(jiàn)絀。
對(duì)于某些無(wú)法 DMA 到未對(duì)齊地址(如 4*n+2 或非以太網(wǎng)硬件)的以太網(wǎng)硬件,這可能是一個(gè)問(wèn)題,然后需要將傳入的幀復(fù)制到對(duì)齊的緩沖區(qū)中。 因?yàn)檫@在可以進(jìn)行非對(duì)齊訪問(wèn)的架構(gòu)上是不必要的,所以代碼可以像這樣依賴于 CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS:
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESSskb = original skb #elseskb = copy skb #endif譯注:
其他非對(duì)齊訪問(wèn)資料:談?wù)剝?nèi)存對(duì)齊
總結(jié)
- 上一篇: jQuery字体自动翻动
- 下一篇: 微信小程序基于vant的自定义底部导航栏