內(nèi)存對(duì)齊原理
內(nèi)存對(duì)齊原理
對(duì)于程序而言,一個(gè)變量的數(shù)據(jù)存儲(chǔ)范圍是在一個(gè)尋址步長(zhǎng)范圍內(nèi)的話,這樣一次尋址就可以讀取到變量的值,如果是超出了步長(zhǎng)范圍內(nèi)的數(shù)據(jù)存儲(chǔ),就需要讀取兩次尋址再進(jìn)行數(shù)據(jù)的拼接,效率明顯降低了。例如一個(gè)double類型的數(shù)據(jù)在內(nèi)存中占據(jù)8個(gè)字節(jié),如果地址是8,那么好辦,一次尋址就可以了,如果是20呢,那就需要進(jìn)行兩次尋址了。
這樣就產(chǎn)生了數(shù)據(jù)對(duì)齊的規(guī)則,也就是將數(shù)據(jù)盡量的存儲(chǔ)在一個(gè)步長(zhǎng)內(nèi),避免跨步長(zhǎng)的存儲(chǔ),這就是內(nèi)存對(duì)齊。
在32位編譯環(huán)境下默認(rèn)4字節(jié)對(duì)齊,在64位編譯環(huán)境下默認(rèn)8字節(jié)對(duì)齊。 現(xiàn)代處理器一般都有多個(gè)級(jí)別的高速緩存,處理器訪問(wèn)這些高速緩存里的數(shù)據(jù)的效率要比訪問(wèn)內(nèi)存里的數(shù)據(jù)效率高得多(就像處理器訪問(wèn)內(nèi)存里的數(shù)據(jù),比訪問(wèn)磁盤里的數(shù)據(jù)效率高得多一樣。 就像上面介紹以的一樣,一般來(lái)說(shuō),CPU 總是以字大?。?2 位處理器上常常為 4 個(gè)字節(jié))訪問(wèn)數(shù)據(jù),所以如果數(shù)據(jù)沒有內(nèi)存對(duì)齊,CPU 訪問(wèn)這些數(shù)據(jù)時(shí),可能就需要執(zhí)行更多次的讀取操作才行。
在這樣的機(jī)器上,讀取 2 個(gè)字節(jié)數(shù)據(jù)往往比讀取 4 個(gè)字節(jié)數(shù)據(jù)慢得多。 訪問(wèn)范圍提高 對(duì)于任意給定的地址空間,如果體系架構(gòu)可以確定 2 個(gè) LSB 總是 0(例如 32 位機(jī)器),那么它可以訪問(wèn) 4 倍多的內(nèi)存(2 個(gè)位能夠表示 4 個(gè)不同狀態(tài))。從一個(gè)地址中去掉 2 個(gè) LSB,將得到 4 字節(jié)的內(nèi)存對(duì)齊,或者說(shuō)“跨距”,因?yàn)榈刂访吭黾右?,它就有效的增?bit 2,而不是 bit 0。
(鑒于低 2 位總是 00) 這甚至?xí)绊懴到y(tǒng)的物理設(shè)計(jì):如果地址總線的需要少 2 位,CPU 上的管腳就可以少 2 個(gè)。 前面提到 CPU 每次訪問(wèn)數(shù)據(jù)的寬度是一個(gè)字,如果C語(yǔ)言程序中的數(shù)據(jù)總是內(nèi)存對(duì)齊的,那么 CPU 訪問(wèn)數(shù)據(jù)總是原子性的,這對(duì)于許多無(wú)鎖數(shù)據(jù)結(jié)構(gòu)和其他并發(fā)需求的正確操作至關(guān)重要。 規(guī)則: 1、數(shù)據(jù)成員對(duì)齊規(guī)則: 結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,**個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員的對(duì)齊按照#pragma pack指定的數(shù)值和這個(gè)數(shù)據(jù)成員自身長(zhǎng)度中,比較小的那個(gè)進(jìn)行。
2、 結(jié)構(gòu)(或聯(lián)合)的 整體對(duì)齊規(guī)則: 在數(shù)據(jù)成員完成各自對(duì)齊之后,結(jié)構(gòu)(或聯(lián)合)本身也要進(jìn)行對(duì)齊,對(duì)齊將按照#pragma pack指定的數(shù)值和結(jié)構(gòu)(或聯(lián)合)**數(shù)據(jù)成員長(zhǎng)度中,比較小的那個(gè)進(jìn)行。 3、 結(jié)合1、2可推斷:當(dāng)#pragma pack的n值等于或超過(guò)所有數(shù)據(jù)成員長(zhǎng)度的時(shí)候,這個(gè)n值的大小將不產(chǎn)生任何效果。 #pragma pack 其實(shí)就是指定內(nèi)存對(duì)齊系數(shù),如1,2,4,8,16。
內(nèi)存對(duì)齊的作用
CPU讀取內(nèi)存粒度一般是2,4,8,16字節(jié),當(dāng)CPU讀取非對(duì)齊內(nèi)存時(shí),有可能需要兩次訪問(wèn),而對(duì)齊內(nèi)存只需要一次。 假設(shè)下面的結(jié)構(gòu)體是按1字節(jié)對(duì)齊,CPU讀取內(nèi)存粒度為4,那么當(dāng)訪問(wèn)數(shù)據(jù)成員c時(shí),CPU需要先讀取前4個(gè)字節(jié)的內(nèi)容,然后再讀取后4字節(jié)的內(nèi)容。
不是所有的硬件平臺(tái)都能訪問(wèn)任意地址上的任意數(shù)據(jù)的,某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
Intel/AMD的x86/x86-64架構(gòu)則始終支持支持非對(duì)齊內(nèi)存訪問(wèn),而在ARM架構(gòu)中,只有ARMv6及以上才支持非對(duì)齊內(nèi)存訪問(wèn)。 每個(gè)數(shù)據(jù)成員的偏移量必須是數(shù)據(jù)成員自身長(zhǎng)度和指定的對(duì)齊長(zhǎng)度中較小的那個(gè)的倍數(shù)。 結(jié)構(gòu)體大小必須是結(jié)構(gòu)體中**數(shù)據(jù)成員長(zhǎng)度和指定的對(duì)齊長(zhǎng)度中較小的那個(gè)的倍數(shù)。
內(nèi)存對(duì)齊問(wèn)題
1.平臺(tái)原因(移植原因): 不是所有的硬件平臺(tái)都能訪問(wèn)任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能 在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。 2.性能原因: 數(shù)據(jù)結(jié)構(gòu)應(yīng)該盡可能地在自然邊界上對(duì)齊。
原因在于,為了訪問(wèn)未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問(wèn);而對(duì)齊的內(nèi)存訪問(wèn)僅需要一次訪問(wèn)。
(如果是對(duì)齊的,那么CPU不需要跨越兩個(gè)操作字,不是對(duì)齊的則需要訪問(wèn)兩個(gè)操作字才能拼接出需要的內(nèi)存地址) 指針的大小一般是一個(gè)機(jī)器字的大小 通過(guò)Go語(yǔ)言的structlayout工具,可以得出下圖 這些類型在之前的 slice 、 map 、 interface 已經(jīng)介紹過(guò)了,也特意強(qiáng)調(diào)過(guò),makehmap函數(shù)返回的是一個(gè)指針,因此map的對(duì)齊為一個(gè)機(jī)器字. 回頭看看 sync.pool的防止copy的空結(jié)構(gòu)體字段,也是放在**位,破案了。 計(jì)算機(jī)結(jié)構(gòu)可能會(huì)要求內(nèi)存地址 進(jìn)行對(duì)齊;也就是說(shuō),一個(gè)變量的地址是一個(gè)因子的倍數(shù),也就是該變量的類型是對(duì)齊值。 函數(shù)Alignof接受一個(gè)表示任何類型變量的表達(dá)式作為參數(shù),并以字節(jié)為單位返回變量(類型)的對(duì)齊值。對(duì)于變量x: 這是因?yàn)閕nt64在bool之后未對(duì)齊。
它是32位對(duì)齊的,但不是64位對(duì)齊的,因?yàn)槲覀兪褂玫氖?2位系統(tǒng),因此實(shí)際上只是兩個(gè)32位值并排在一起。 ● 內(nèi)存對(duì)齊是為了cpu更高效訪問(wèn)內(nèi)存中數(shù)據(jù) ● 結(jié)構(gòu)體對(duì)齊依賴類型的大小保證和對(duì)齊保證 ● 地址對(duì)齊保證是:如果類型 t 的對(duì)齊保證是 n,那么類型 t 的每個(gè)值的地址在運(yùn)行時(shí)必須是 n 的倍數(shù)。
結(jié)構(gòu)體內(nèi)存對(duì)齊
1.什么是內(nèi)存對(duì)齊? 2.為什么要做內(nèi)存對(duì)齊? 3.結(jié)構(gòu)體內(nèi)存對(duì)齊規(guī)則 4.源碼內(nèi)存對(duì)齊算法 計(jì)算機(jī)內(nèi)存都是以字節(jié)為單位劃分的,從理論上講似乎對(duì)任何類型的變量的訪問(wèn)可以從任何地址開始,但是實(shí)際的計(jì)算機(jī)系統(tǒng)對(duì)基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們會(huì)要求這些數(shù)據(jù)的首地址的值是某個(gè)數(shù)k(通常它為4或8的倍數(shù)),這就是所謂的內(nèi)存對(duì)齊。內(nèi)存對(duì)齊是一種在計(jì)算機(jī)內(nèi)存中排列數(shù)據(jù)(表現(xiàn)為變量的地址) 、訪問(wèn)數(shù)據(jù)(表現(xiàn)為CPU讀取數(shù)據(jù))的一種方式。
內(nèi)存對(duì)齊包含了兩種相互獨(dú)立又相互關(guān)聯(lián)的部分:基本數(shù)據(jù)對(duì)齊和結(jié)構(gòu)體數(shù)據(jù)對(duì)齊 。
1.平臺(tái)原因(移植原因):不是所有的硬件平臺(tái)都能訪問(wèn)任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。 2.性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于,為了訪問(wèn)未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問(wèn);而對(duì)齊的內(nèi)存訪問(wèn)僅需要一次訪問(wèn)。 當(dāng)我們定義一個(gè) struct 的時(shí)候,它在內(nèi)存中是怎么存儲(chǔ)的?占用了多少字節(jié)的內(nèi)存空間呢? 從打印結(jié)果看出一個(gè)問(wèn)題,結(jié)構(gòu)體中變量相同,只是順序不同,結(jié)果影響結(jié)構(gòu)體占用內(nèi)存大小,這就是iOS中內(nèi)存對(duì)齊 1.數(shù)據(jù)成員對(duì)?規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第 一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要 從該成員大小或者成員的子成員大小(只要該成員有子成員,比如說(shuō)是數(shù)組, 結(jié)構(gòu)體等)的整數(shù)倍開始(比如int為4字節(jié),則要從4的整數(shù)倍地址開始存 儲(chǔ)。
min(當(dāng)前開始的位置m,n) m=9 n=4 9 10 11 12 2.結(jié)構(gòu)體作為成員:如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從 其內(nèi)部**元素大小的整數(shù)倍地址開始存儲(chǔ).(struct a里存有struct b,b 里有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲(chǔ).) 3.收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,.必須是其內(nèi)部** 成員的整數(shù)倍.不足的要補(bǔ)?。 拿上面的兩個(gè)結(jié)構(gòu)體舉例說(shuō)明 sizeof 最終得到的結(jié)果是該數(shù)據(jù)類型占用空間的大小 class百科_getInstanceSize 獲取類的實(shí)例對(duì)象所占用的內(nèi)存大小 malloc_size 獲取系統(tǒng)實(shí)際分配的內(nèi)存大小 算法原理:k + 15 >> 4 << 4 ,其中 右移4 + 左移4相當(dāng)于將后4位抹零,跟 k/16 * 16一樣 ,是16字節(jié)對(duì)齊算法,小于16就成0了 對(duì)于一個(gè)對(duì)象來(lái)說(shuō),其真正的對(duì)齊方式 是 8字節(jié)對(duì)齊,8字節(jié)對(duì)齊已經(jīng)足夠滿足對(duì)象的需求了 apple系統(tǒng)為了防止一切的容錯(cuò),采用的是16字節(jié)對(duì)齊的內(nèi)存,主要是因?yàn)椴捎?字節(jié)對(duì)齊時(shí),兩個(gè)對(duì)象的內(nèi)存會(huì)緊挨著,顯得比較緊湊,而16字節(jié)比較寬松,利于蘋果以后的擴(kuò)展。