天天看點

c# 結構體 4位元組對齊_第4篇:C/C++ 結構體及其數組的記憶體對齊

資料對象

本節談及的記憶體對齊,而在進行這個話題之前,我先引入一個叫

資料對象

的概念。

大部份C語言教程的文章很少會提的一個概念就是資料對象(Data Object),簡稱

對象

,像基于C衍生出來其他高層語言所了解的"對象"是有些差別的.C中的對象更偏向于記憶體模型,同樣C中的資料對象也适用于彙編,本來C就是"結構化"的彙編語言.資料對象就是本身兩個屬性.

  • 資料值(value)
  • 存儲位址 (storage location)

也就是C中支援的所有資料類型定義出來的變量或由基本資料類型組合構造的使用者定義類型(類類型/也叫結構體),都統稱

資料對象

,

一切類型皆為對象

。而被記憶體對齊的正是

資料對象

記憶體對齊

也叫

位元組對齊

(data aligment):就是資料對象的記憶體大小可以被2的N次方的整數整除,也就是說位元組對齊可以用某個2的N次方的整數去對齊.

目前計算機的32位的CPU可以在每個時刻周期從記憶體讀取4個位元組并填充資料總線,而64位的CPU每個時刻周期可以讀取8個位元組,而C語言的的設計者為了遵循CPU的這種特性。就給C編譯器,當然後來的C++編譯器也繼承這一特性,在對C/C++源碼編譯的時候會對源碼中的資料對象自動執行記憶體對齊操作(對

資料對象

之前

填充

一些沒用的位元組塊)。

備注:上面部分術語摘自相關的維基資料,很官腔套話是吧?!下面來些實質上示例講解。

為什麼要記憶體對齊?

因為我們要通路實體記憶體并能夠在一次通路中擷取整個資料,是以想象以下我們要從記憶體中讀取一個4位元組的int,而前兩個位元組在記憶體中的一個字中,而後兩個位元組在另外一個字中。我們不想分兩次讀取兩個字然後将位元組讀取的位元組再次裝拼成一個整數,這是一種低效的記憶體通路。

底效的記憶體通路示範
c# 結構體 4位元組對齊_第4篇:C/C++ 結構體及其數組的記憶體對齊

未對齊的Data CPU多次加載word示例

這是是位于half-word邊界上對齊的未對齊word。 為了對此進行操作,CPU必須做兩個word加載。一個用于上half-word,一個用于下half-word。這兩項加載操作将進入兩個獨立的寄存器。然後分别将高層的word執行位移兩個位元組向上(同理位于底層位址的word執行向低層位址位移兩個位元組,上層half-word然後低層half-word寄存器組合。這需要大約四個額外的操作來與未對齊的記憶體資料進行互動,隻是為了進入寄存器進行處理。與一次性加載指令相比,可以看出這是一個很大的CPU開銷。

是以為了有效地一次性執行記憶體通路:直接讀取四個位元組或八個位元組。就有了記憶體對齊這個玩意了。

記憶體對齊規律

  1. 一般來說,對于需要X位元組的原始資料類型,位址必須是X的倍數 .
  2. struct的起始位址決于其成員變量中的資料類型的sizeof()最大值作為對齊條件
  3. 編譯器在編譯階段對struct中未使用的記憶體空間執行填充操作,以確定字段對齊
  4. char類型不需要對齊,可自由配置設定任意可用的1位元組尺寸的記憶體空間之中.

基本資料類型的對齊要求

在不同的硬體架構和OS上執行的記憶體對齊是不一樣,我們下面有個表,

c# 結構體 4位元組對齊_第4篇:C/C++ 結構體及其數組的記憶體對齊

需要特别指出的是

  • 對于char類型沒有任何對齊要求。
  • 對于double類型即便是IA32的硬體架構,事實上可以通過gcc編譯的時候指定指令行選項-malign-double,double類型也會以8位元組對齊而不是4位元組對齊。

    從這個表可知,不同類型的CPU,對齊操作是不一樣的.

x86_64環境下的對齊示例

以下示例的左側的數字表示記憶體位址,為了一目了然,使用十進制表示,并且在IA32 Windows 或 x86_64環境下,這個示例主要用來解析上面的對齊規則。

struct 
           
  • 首先,在結構體内部,必須滿足每個成員的對齊條件
  • 縱觀整個struct的成員變量
    • 每個結構體都由一個對齊條件整數K,K即是結構體中所有成員變量中的類型尺寸的最大值。
    • 起始位址和結構體的長度必須是整數K的倍數。
c# 結構體 4位元組對齊_第4篇:C/C++ 結構體及其數組的記憶體對齊

那麼該結構體的對齊條件整數K是多少?

  • 根據規則1和2:由于這個結構體的對齊條件是8位元組,因為它是struct成員變量中對齊條件最大是8個位元組,是以這個結構體的起始位址必須是8的倍數,從低位r位址算起當然是位址160.
  • 根據規則4:由于char類型是沒有任何對齊條件的限制,并且在結構體中第一個聲明的變量,是以它就落在結構體的起始位址,也就是位址160的位置.
  • 根據規則1,由于int數組類型的每個元素占用4個位元組,那麼int類型的起始位址以4的倍數開始的那麼就自然落到了164這個位址,緊挨着的i[2]元素自然落在168這個位址.
  • 成員變量c和int[0]之間有還有未使用的記憶體空間,根據規則3,會填充未使用的位元組.
  • 根據規則1,Double類型的成員以8的倍數對齊,自然落在自struct起始位址起第16個位元組的位置,即位址176的地方算起占用8個位元組表示成員變量d的資料.
  • 根據規則3,另外編譯器會填充成員變量d和數組元素int[1]之間未使用的記憶體位置.
IA32 Linux環境下

,上面示例對齊條件整數K是多少呢?你可以自行思考一下。

節省記憶體空間

從上面的例子我們得知,雖然記憶體對齊操作有助于優化CPU對記憶體的通路,但會帶來一下副作用,就是會浪費一些記憶體空間.但這個問題不能全賴在編譯器身上,作為程式員不良的寫碼習慣也是很大關系滴!!

我們在看看下面的示例從下圖可以得知

結構體内部成員變量聲明的先後順序和編譯器對記憶體對齊後,結構體所占的記憶體空間有很大的關系

.

  • 下圖左手邊的記憶體布局是x86_64架構下對應的C代碼
struct
           
  • 下圖右手邊的記憶體布局是x86_64架構下對應的C代碼
struct
           
c# 結構體 4位元組對齊_第4篇:C/C++ 結構體及其數組的記憶體對齊

由上面的的記憶體布局對比,我們得到得到一個基本結論:

在struct内部,将成員變量按照其類型的sizeof()值由大到小,依存聲明的話能夠不僅可以最大限度地減少編譯器填充未使用記憶體塊的操作,而且填充的記憶體塊出現的次數越少,那麼CPU每次從對應記憶體塊中加載資料到寄存器中執行shift運算的次數也會相應地減少。同時能夠兼顧CPU對對齊記憶體的通路.

有讀者對我上面的結果提出疑問,無論在32位還是64位計算機上,sizeof(double)始終為8個位元組。 不同之處在于在32位Linux系統上,double對齊倍數是4位元組資料類型一樣。 要将double對齊的倍數為8個位元組,請使用-malign-double(編譯時選項)。

結構體數組的記憶體分布

灰常不幸的是!!,結構體的數組是無法滿足上面的所講的節省記憶體的特性的。

typedef 
           
c# 結構體 4位元組對齊_第4篇:C/C++ 結構體及其數組的記憶體對齊

我們已經知道結構體b的對齊條件是8的倍數,由于結構體b的占據17個位元組的記憶體空間,是以和它相關的數組中的每個元素占用記憶體空間必須要達到24個位元組,才能達成每個元素的對齊條件是8的倍數, 是以每個結構體元素,編譯器還需要為每個元素填充7個位元組囧rz...

繼續閱讀