記憶體對齊分配策略(含位域模式)

来源:http://www.cnblogs.com/chaozhu/archive/2016/06/20/5600496.html
-Advertisement-
Play Games

1:記憶體對齊定義: 現在使用的電腦中記憶體空間都是按照位元組劃分的,從理論上講似乎對任何類型的變數的訪問可以從任何地址開始,但是實際上電腦系統對於基本數據類型在記憶體 中的存放位置都有限制,要求這些數據存儲首地址是某個數K的倍數,這樣各種基本數據類型在記憶體沖就是按照一定的規則排列的,而不是一個緊挨著一 ...


1:記憶體對齊定義:
    現在使用的電腦中記憶體空間都是按照位元組劃分的,從理論上講似乎對任何類型的變數的訪問可以從任何地址開始,但是實際上電腦系統對於基本數據類型在記憶體 中的存放位置都有限制,要求這些數據存儲首地址是某個數K的倍數,這樣各種基本數據類型在記憶體沖就是按照一定的規則排列的,而不是一個緊挨著一個排放,這 就是記憶體對齊。

對齊模數:
    記憶體對齊中指定的對齊數值K成為對齊模數(Alignment Modulus)。當一種類型S的對齊模數與另一種類型T的對齊模數的比值是大於1的整數,我們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬鬆)。

2:記憶體對齊的好處:
    記憶體對齊作為一種強制的要求,第一簡化了處理器與記憶體之間傳輸系統的設計,第二可以提升讀取數據的速度。各個硬體平臺對存儲空間的處理上有很大的不同。一 些平臺對某些特定類型的數據只能從某些特定地址開始存取。比如有些架構的CPU在訪問一個沒有進行對齊的變數的時候會發生錯誤,那麼在這種架構下編程必須 保證位元組對齊.其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是 從偶地址開始,如果一個int型(假設為32位系統)如果存放在偶地址開始的地方,那麼一個讀周期就可以讀出這32bit,而如果存放在奇地址開始的地 方,就需要2個讀周期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該32bit數據。顯然在讀取效率上下降很多。
    Intel的IA32架構的處理器則不管數據是否對齊都能正確工作。但是如果想提升性能,應該註意記憶體對齊方式。
ANSI C標準中並沒有規定相鄰聲明的變數在記憶體中一定要相鄰。為了程式的高效性,記憶體對齊問題由編譯器自行靈活處理,這樣導致相鄰的變數之間可能會有一些填充字 節。對於基本數據類型(int char等),他們占用的記憶體空間在一個確定硬體系統下有確定的值。ANSI C規定一種結構類型的大小是它所有欄位的大小以及欄位之間或欄位尾部的填充區大小之和。

3:記憶體對齊策略:
微軟C編譯器(cl.exe for 80×86)的對齊策略:
第一: 結構體變數的首地址能夠被其最寬基本類型成員的大小所整除;
備註:編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本數據類型,然後尋找記憶體地址能被該基本數據類型所整除的位置,作為結構體的首地址。將這個最寬的基本數據類型的大小作為上面介紹的對齊模數。
第二: 結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組(internal adding);
備註:為結構體的一個成員開闢空間之前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是否是本成員的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的位元組,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個位元組。
第三: 結構體的總大小為結構體最寬基本類型成員大小的整數倍,如有需要,編譯器會在最末一個成員之後加上填充位元組(trailing padding)。
備註:結構體總大小是包括填充位元組,最後一個成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最後填充幾個位元組以達到本條要求。

填充位元組就是為了使結構體欄位滿足記憶體對齊要求而額外分配給結構體的空間。對於結構體本身也存在著對齊要求,ANSI C標準規定結構體類型的對齊要求不能比它所有欄位中要求最嚴格的那個寬鬆,但是可以更嚴格(但此非強制要求,VC7.1就僅僅是讓它們一樣嚴格)。C標準 保證,任何類型(包括自定義結構類型)的數組所占空間的大小一定等於一個單獨的該類型數據的大小乘以數組元素的個數。換句話說,數組各元素之間不會有空 隙。

總結規則如下:
0: 結構體變數的首地址能夠被其最寬基本類型成員的大小所整除
1: VC6和VC71預設的記憶體對齊方式是 #pragam pack(8)
2: 結構體中每個成員按其類型的對齊參數(通常是這個類型的大小)和指定對齊參數中較小的一個對齊.
3:   結構體每個成員相對於結構體首地址的偏移量都是成員大小的整數倍.
4:   結構體本身也存在著對齊要求規則,不能比它所有欄位中要求最嚴格的那個寬鬆.
5: 結構體的總大小為結構體最寬基本類型成員大小的整數倍,且應儘量節省記憶體。
6: 在GCC中,對齊模數的準則是:對齊模數最大隻能是 4,也就是說,即使結構體中有double類型,對齊模數還是4,所以對齊模數只能是1,2,4。
      而且在上述的規則中,第3條里,offset必須是成員大小的整數倍:
       (1): 如果這個成員大小小於等於4則按照上述準則是可行的,
       (2): 如果成員的大小大於4,則結構體每個成員相對於結構體首地址的偏移量只能按照是4的整數倍來進行判斷是否添加填充。

typedef struct ms1 {
  char a;
  int b;
} MS1;

typedef struct ms2 {
  int a;
  char b;
} MS2; 

MS1中有最強對齊要求的是b欄位(int類型),欄位a相對於首地址偏移量為0(1的倍數),直接存放,此時如果直接存放欄位b,則欄位b相對於結構體 變數首地址的偏移量為1(不是4的倍數),填充3位元組,b由偏移地址為4開始存放。也就是遵循了第2條與第3條規則,而對於結構體變數本身,根據規則4, 對齊參數至少應該為4。根據規則5,sizeof(MS1) = 8; 同樣MS2分析得到的結果也是如此。

typedef struct ms3 {
  char a;
  short b;
  double c;
} MS3;

typedef struct ms4 {
  char a;
  MS3 b;
} MS4; 

MS3中記憶體要求最嚴格的欄位是c(8位元組),MS3的對齊參數也是8位元組; 那麼MS4類型數據的對齊模數就與MS3中的double一致(為8),a欄位後面應填充7個位元組.sizeof(MS3) = 16; sizeof(MS4) = 24;
註意規則5中是說,結構體的總大小為結構體最寬基本類型成員大小的整數倍。註意是基本類型,這裡的MS3不是基本類型。
對齊模數的選擇只能是根據基本數據類型,所以對於結構體中嵌套結構體,只能考慮其拆分的基本數據類型。

例子3(GCC):
struct T {
  char ch;
  double d ;
}; 

在GCC下,sizeof(T)應該等於12個位元組。VC8下為16位元組。
ch為1位元組,沒有問題的,之後d的大小大於4,對於d的對齊模數只能是4,相對於結構體變數的首地址偏移量也只能是4,而不能使8的整數倍,由偏移量4開始存放,結構體共占12位元組。
這裡並沒有執行第5條規則。

位域情況
C99規定int、unsigned   int和bool可以作為位域類型。但編譯器幾乎都對此作了擴展,允許其它類型類型的存在。
如果結構體中含有位域(bit-field),總結規則如下
1) 如果相鄰位域欄位的類型相同,且其位寬之和小於類型的sizeof大小,則後面的欄位將緊鄰前一個欄位存儲,直到不能容納為止;
2) 如果相鄰位域欄位的類型相同,但其位寬之和大於類型的sizeof大小,則後面的欄位將從新的存儲單元開始,其偏移量為其類型大小的整數倍;
3) 如果相鄰的位域欄位的類型不同,則各編譯器的具體實現有差異,VC6採取不壓縮方式(不同位域欄位存放在不同的位域類型位元組中),Dev-C++和GCC都採取壓縮方式;
4)如果位域欄位之間穿插著非位域欄位,則不進行壓縮;
5) 結構體的總大小為結構體最寬基本類型成員大小的整數倍,且應儘量節省記憶體。
備註:當兩欄位類型不一樣的時候,對於不壓縮方式,例如:

struct N {
  char c:2;
  int i:4;
}; 

依然要滿足不含位域結構體記憶體對齊準則第3條,i成員相對於結構體首地址的偏移應該是4的整數倍,所以c成員後要填充3個位元組,然後再開闢4個位元組的空間作為int型,其中4位用來存放i,所以上面結構體在VC中所占空間為8個位元組;
而對於採用壓縮方式的編譯器來說,遵循不含位域結構體記憶體對齊準則第3條,不同的是,如果填充的3個位元組能容納後面成員的位,則壓縮到填充位元組中,不能容納,則要單獨開闢空間,所以上面結構體N在GCC或者Dev- C++中所占空間應該是4個位元組。

例子4:
typedef struct {
  char c:2;
  double i;
  int c2:4;
}N3; 

按照含位域規則4,在GCC下占據的空間為16位元組,在VC下占據的空間是24個位元組。 結論:
--------
定義結構體的時候,成員最好能從大到小來定義,那樣能相對的省空間。例如如下定義:

struct A {
  double d;
  int i;
  char c;
}; 

那麼,無論是windows下的vc系列編譯器,還是linux下的gcc,都是16位元組。

例子5:
typedef union student{
  char name[10];
  long sno;
  char sex;
  float score [4];
} STU;
STU aa[5];
cout<<sizeof(aa)<<endl;

union是可變的以其成員中最大的成員作為該union的大小16*5=5=80

例子6:
typedef struct student{
  char name[10];
  long sno;
  char sex;
  float score [4];
} STU;
STU aa[5];
cout<<sizeof(aa)<<endl; 

STU占空間為:10位元組(char)+空2位元組+4位元組(long)+1位元組(char)+空3位元組+16位元組(float)=36位元組,36*5=180位元組

例子7(VC8.0):
typedef struct bitstruct {
  int b1:5;
  int b2:2;
  int b3:3;
}bitstruct;

int _tmain(int argc, _TCHAR* argv[]) {
  bitstruct b;
  memcpy(&b,"EM",sizeof(b));
  cout<<sizeof(b)<<endl;
  cout<<b.b1<<endl<<b.b2<<endl<<b.b3;
  return 0;
} 

對於bitstruct是含有位域的結構體,sizeof(int)為4位元組,按照規則1、2,首先b1占起始的5個位元組, 根據含位域規則1, b2緊跟存放,b3也是緊跟存放的。
根據規則5,得到sizeof(bitstruct) = 4。
現在主流的CPU,intel系列的是採用的little endian的格式存放數據,motorola系列的CPU採用的是big endian.
以主流的little endian分析:
在進行記憶體分配的時候,首先分配bitstruct的第一個成員類型int(4位元組),這四個位元組的存放按照低位元組存儲在低地址中的原則。
int共4個位元組:
第4個位元組 - 第3個位元組 - 第2個位元組 - 第1個位元組,

在記憶體中的存放方式如下所示。
而後為b1分配5位,這裡優先分配的應該是低5位,也就是第一個位元組的低5位。
繼而分配b2的2個位元組,也就是第1個位元組中緊接著的2位。
最後分配b3的3位,按照規則1、2,b3還是緊接著存放的,b3的最低位是第一個位元組的最高位,高兩位為第2個位元組的低兩位。
記憶體分配圖如下所示:

QQ截圖未命名

字元E二進位為0100 0101,字元M的二進位為0100 1101,在記憶體中存放如下所示:

QQ截圖未命名2
memcpy為按位拷貝的,所以兩片記憶體區可以直接對應上,得到
b1的二進位形式為:00101 ,高位為0,正數,5
b2的二進位形式為:10 ,高位為1,負數,取反加1,添加符號,-2
b3的二進位形式為:b3的最低一位是0,高位為01,拼接後為010,正數,2

記憶體分配情況感覺蠻奇怪的,按如下修改例7,b1應該為5,b2為-2,b3為-6,VC8.0下驗證正確。

typedef struct bitstruct {
  int b1:5;
  int b2:2;
  int b3:4;
}bitstruct;

int _tmain(int argc, _TCHAR* argv[]) {
  bitstruct b;
  memcpy(&b,"EM",sizeof(b));
  cout<<sizeof(b)<<endl;
  cout<<b.b1<<endl<<b.b2<<endl<<b.b3;
  return 0;
}

4: 定義數組時的記憶體佈局及記憶體位元組對齊

int b=10;

int a[3]={1,2,3};

int c=11;

image

int b=0x01020304;

char ch='a';

對於一個數0x01020304; 對於一個數0x1122

使用Little Endian方式時,低地址存放低位元組,由低地址向高地址存放為:4->3->2->1
而使用Big Endian方式時, 低地址存放高位元組,由低地址向高地址存放為:1->2->3->4

 

而在Little Endian模式中,b的地址所指的就是 : 低地址(存放的是最低的位元組)

 

image

 1 void __cdecl func_cdcel(int i, char *szTest) {
 2 
 3       cout << "szTest在棧中的地址:" << &szTest << endl;
 4 
 5       cout << "szTest本身的值(指向的地址):" << (void*)szTest << endl<<endl;
 6 
 7       
 8 
 9       cout << "i在堆棧中地址:" << &i << endl;
10 
11       cout << "i的地址:" << &i << endl;
12 
13    
14 
15       int k,k2;
16 
17       cout << "局部變數k的地址:" << &k << endl;
18 
19       cout << "局部變數k2的地址:" << &k2 << endl;
20 
21       cout << "-------------------------------------------------------" << endl;
22 
23   }
24 
25    
26 
27   void __stdcall func_stdcall(int i, char *szTest){
28 
29       cout << "szTest在棧中的地址:" << &szTest << endl;
30 
31       cout << "szTest本身的值(指向的地址):" << (void*)szTest << endl<<endl;
32 
33    
34 
35       cout << "i在堆棧中地址:" << &i << endl;
36 
37       cout << "i的地址:" << &i << endl;
38 
39    
40 
41       int k,k2;
42 
43       cout << "局部變數k的地址:" << &k << endl;
44 
45       cout << "局部變數k2的地址:" << &k2 << endl;
46 
47       cout << "-------------------------------------------------------" << endl;
48 
49   }
50 
51   
52 
53   int main(){
54 
55       int a[4];
56 
57       cout <<"a[0]地址:"<< &a[0] << endl;
58 
59       cout <<"a[1]地址:"<< &a[1] << endl;
60 
61       cout <<"a[2]地址:"<< &a[2] << endl;
62 
63       cout <<"a[3]地址:"<< &a[3] << endl;
64 
65    
66 
67       int i = 0x22;
68 
69       int j = 8;
70 
71       char szTest[4] = {'a','b', 'c', 'd'};
72 
73       cout <<"i的地址:"<<&i << endl;
74 
75       cout <<"szTest的地址:"<<(void*)szTest << endl;
76 
77       func_cdcel(i, szTest);
78 
79       func_stdcall(i, szTest);
80 
81  }
View Code

輸出為:

a[0]地址:0012FF54
a[1]地址:0012FF58
a[2]地址:0012FF5C
a[3]地址:0012FF60                  <— 可見存儲方式如上圖所示,a[3]在高地址,先入棧,而數組地址a為a[0]的地址(低地址)
i的地址:0012FF48                    <— 這裡進行了記憶體對齊,i的起始地址必定是i所占記憶體大小的倍數
szTest的地址:0012FF30  

szTest在棧中的地址:0012FE5C  
szTest本身的值(指向的地址):0012FF30

i在堆棧中地址:0012FE58           <— i在堆棧中的地址低於szTest,也就是說szTest是先入棧的
i的地址:0012FE58
局部變數k的地址:0012FE48
局部變數k2的地址:0012FE3C
-------------------------------------------------------
szTest在棧中的地址:0012FE5C
szTest本身的值(指向的地址):0012FF30

i在堆棧中地址:0012FE58
i的地址:0012FE58
局部變數k的地址:0012FE48
局部變數k2的地址:0012FE3C

 

轉載:http://www.cnblogs.com/alex-tech/archive/2011/03/24/1993856.html

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、簡單文件系統操作 : df ( h) 查看磁碟容量; rootfs作為系統啟動時內核載入記憶體之後,在掛載真正的磁碟之前的一個臨時文件系統; /dev/sda2 對應主機硬碟的分區,後面的a表示第幾塊硬碟,2表示分區; du查看目錄的容量, h更易讀的方式展示(預設以blocks的大小展示), d ...
  • linux系統通過setup命令可以調用圖形界面來配置網路,該命令只要centos等版本才有。 ...
  • 游標控制 文件操作 :w 寫文件 :w! 寫文件,忽略警告信息:wq 寫文件之後退出編輯:q 退出編輯器:q! 強制退出編輯器ZZ 退出編輯器,如果文件有改動,則保存再退出:x 退出編輯器,如果文件有改動,則保存再退出:e! 重新從磁碟載入文件:n 編輯地下一行:n! 編輯地下一行(忽略警告) 內容 ...
  • 目前我知道的方法有四種 1.awk 'END {print}' 2.sed -n '$p' 3.sed '$!N;$!D' 4.awk '{b=a"\n"$0;a=$0}END{print b}' ...
  • MSP430 ...
  • atexit函數 atexit函數的原型如下 void atexit(void (*func)(void)) 它是一個參數為返回值和參數均為空的函數指針的函數,含義是當前進程結束之前執行參數函數指針所指向的函數,使用的時候要在main中註冊,一次可以註冊很多函數,函數的執行順序與註冊的先後有關,關係 ...
  • 虛擬化對於計算的抽象,大家可能相對熟悉,也許都有在單機使用諸如Virtual PC或者Virtual Box的經驗。使用的這些虛擬化軟體的第一印象就是我們的CPU可以同時運行多套不同的操作系統,並且其上應用程式並行不悖。計算的抽象使得同一套硬體設備上的操作系統之間得以相互隔離,猶如一個身體擁有兩個甚... ...
  • 在Linux系統管理中,有時候需要設置賬號密碼複雜度(長度)、密碼過期策略等,這個主要是由/etc/login.defs參數文件中的一些參數控制的的。它主要用於用戶賬號限制,裡面的參數主要有下麵一些: /etc/login.defs: # Password aging controls:## PAS... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...