C、C++内存对齐

2023-11-12

文章转载自:http://www.jellythink.com/archives/413

#include <iostream>
using namespace std;

struct Test_A
{
     char a;
     char b;
     int c;
};

struct Test_B
{
     char a;
     int c;
     char b;
};

struct Test_C
{
     int c;
     char a;
     char b;
};

int main()
{
     struct Test_A a;
     memset(&a, 0, sizeof(a));

     struct Test_B b;
     memset(&b, 0, sizeof(b));

     struct Test_C c;
     memset(&c, 0, sizeof(c));

     // Print the memory size of the struct
     cout<<sizeof(a)<<endl;
     cout<<sizeof(b)<<endl;
     cout<<sizeof(c)<<endl;

     return 0;
}

好了,一段简单的程序,上面的这段程序输出是什么?如果你很懂,也就会知道我接下来要讲什么了,可以略过了;如果,你不知道,或者还很模糊,请继续阅读。

 

这是为什么?

上面这段程序的输出结果如下(windows 8.1 + visual studio 2012 update3下运行):

// Print the memory size of the struct
cout<< sizeof(a)<<endl; // 8bytes
cout<< sizeof(b)<<endl; // 12bytes
cout<< sizeof(c)<<endl; // 8bytes

很奇怪么?定义的三个结构体,只是换了一下结构体中定义的成员的先后顺序,怎么最终得到的结构体所占用的内存大小却不一样呢?很诡异么?好了,这就是我这里要总结的内存对齐概念了。

 

内存对齐

内存对齐的问题主要存在于理解struct和union等复合结构在内存中的分布。许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。这个值k在不同的CPU平台下,不同的编译器下表现也有所不同,现在我们涉及的主流的编译器是Microsoft的编译器和GCC。

对于我们这种做上层应用的程序员来说,真的是很少考虑内存对齐这个问题的,内存对齐对于上层程序员来说,是“透明的”。内存对齐,可以说是编译器做的工作,编译器为程序中的每个数据块安排在适当的内存位置上。很多时候,我们要写出效率更高的代码,此时我们就需要去了解这种内存对齐的概念,以及编译器在后面到底偷偷摸摸干了点什么。特别是对于C和C++程序员来说,理解和掌握内存对齐更是重要的。

为什么要有内存对齐呢?该占用多大的内存,那就开辟对应大小的内存就好了,好比上面的结构体,两个char类型和一个int类型,大小应该是6bytes才对啊,怎么又是8bytes,又是12bytes的啊?对于内存对齐,主要是为了提高程序的性能,数据结构,特别是栈,应该尽可能地在自然边界上对齐。原因在于,为了访问未对其的内存,处理器需要做两次内存访问;然而,对齐的内存访问仅仅需要一次内存访问。

在计算机中,字、双字和四字在自然边界上不需要在内存中对齐(对字、双字和四字来说,自然边界分别是偶数地址,可以被4整除的地址和可以被8整除的地址)。如果一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,就被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数,但却没有跨越字边界,就被认为是对齐的,能够在一个总线周期中被访问。综上所述,内存对齐可以用一句话来概括——数据项只能存储在地址是数据项大小的整数倍的内存位置上。

我们再来看看一个简答的例子:

#include <stdio.h>

struct Test
{
     char a;
     int b;
     int c;
     char d;
};

int main()
{
     struct Test structTest;
     printf("&a=%p\n", &structTest.a);
     printf("&b=%p\n", &structTest.b);
     printf("&c=%p\n", &structTest.c);
     printf("&d=%p\n", &structTest.d);

     printf("sizeof(Test)=%d\n", sizeof(structTest));
     return 0;
}

输出结果如下:

&a=00C7FA44
&b=00C7FA48
&c=00C7FA4C
&d=00C7FA50
sizeof(Test)=16

结构体Test的成员变量b占用字节数为4bytes,所以只能存储在4的整数倍的位置上,由于a只占用1一个字节,而a的地址00C7FA44和b的地址00C7FA48之间相差4bytes,这就说明,a其实也占用了4个字节,这样才能保证b的起始地址是4的整数倍。这就是内存对齐。如果没有内存对齐,我们再拿上面的代码作为例子,则可能输出结果如下:

&a=ffbff5e8
&b=ffbff5e9
&c=ffbff5ed
&d=ffbff5f1
sizeof(Test)=10

可以看到,a占用了一个字节,紧接着a之后就是b;之前也说了,内存对齐是操作系统为了快速访问内存而采用的一种策略,简单来说,就是为了防止变量的二次访问。操作系统在访问内存时,每次读取一定的长度(这个长度就是操作系统的默认对齐系数,或者是默认对齐系数的整数倍)。没有了内存对齐,当我们读取变量c时,第一次读取0xffbff5e8~0xffbff5ef的内存,第二次读取0xffbff5f0~0xffbff5f8的内存,由于变量c所占用的内存跨越了两片地址区域,为了正确得到变量c的值,就需要读取两次,将两次内存合并进行整合,这样就降低了内存的访问效率。

我在这里说了这么多,也挺绕口,这就是内存对齐的规则。在C++中,每个特定平台上的编译器都有自己的内存对齐规则,下面我们就内存对齐的规则进行总结。

 

内存对齐规则

每个特定平台上的编译器都有自己的默认“对齐系数”。我们可以通过预编译命令#pragma pack(k),k=1,2,4,8,16来改变这个系数,其中k就是需要指定的“对齐系数”;也可以使用#pragma pack()取消自定义字节对齐方式。具体的对齐规则如下:

规则1:struct或者union的数据成员对齐规则:第一个数据成员放在offset为0的地方,对齐按照#pragma pack指定的数值和自身占用字节数中,二者比较小的那个进行对齐;比如;

#pragma pack(4) // 指定对齐系数为4,当占用字节数大于等于4时,就按照4进行对齐
struct Test
{
     char x1;
     short x2;
     float x3;
     char x4;
};

x1占用字节数为1,1 < 4,按照对齐系数1进行对齐,所以x1放置在offset为0的位置;

x2占用字节数为2,2 < 4,按照对齐系数2进行对齐,所以x2放置在offset为2,3的位置;
x3占用字节数为4,4 = 4,按照对齐系数4进行对齐,所以x3放置在offset为4,5,6,7的位置;
x4占用字节数为1,1 < 4,按照对齐系数1进行对齐,所以x4放置在offset为8的位置;
现在已经占了9bytes的内存空间了,但是实际在visual studio 2012中实测为12bytes,为什么呢?看下一条规则。

规则2:struct或者union的整体对齐规则:在数据成员完成各自对齐以后,struct或者union本身也要进行对齐,对齐将按照#pragma pack指定的数值和struct或者union中最大数据成员长度中比较小的那个进行;

继续使用规则1种的例子进行解释,按照规则1的理解,struct Test已经占用了9bytes,实际为什么是12bytes呢?根据规则2,在所有成员对齐完成以后,struct或者union自身也要进行对齐;我们设定的对齐系数为4,而struct Test中占用字节数最大的是float类型的x3,由于x3占用字节数小于或等于设定的对齐系数4,所以struct或者union整体需要按照4bytes进行对齐,也就是说,struct或者union占用的字节数必须能够被4整除,好了。struct Test已经占用了9bytes了,10bytes不能被4整除,11bytes也不能,12bytes正好;所以,struct Test最终占用的字节数为12bytes。

上述两条规则就是内存对齐的基本规则,先局部对齐,后整体对齐。

 

实例分析

总结了那么多的规则,不来点实际的code,总觉的少点什么,好吧。以下就按照上述总结的内存对齐规则,来进行一些实际的代码分析(注:测试环境Windows 8.1 + Visual Studio 2012 update 3)。

测试代码如下,先确认测试环境:

#include <iostream>
using namespace std;

struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

int main()
{
     cout<<"sizeof(char)"<<sizeof(char)<<endl;          // 1byte
     cout<<"sizeof(short)"<<sizeof(short)<<endl;        // 2bytes
     cout<<"sizeof(int)"<<sizeof(int)<<endl;            // 4bytes
     cout<<"sizeof(double)"<<sizeof(double)<<endl;      // 8bytes
     return 0;
}

 

我分别设置#pragma pack(k),k=1,2,4,8,16进行测试。

#pragma pack(1) // 设定对齐系数为1
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:

x1 <= 1,按照1进行对齐,x1占用0;
x2 > 1,按照1进行对齐,x2占用1,2,3,4,5,6,7,8;
x3 > 1,按照1进行对齐,x3占用9,10;
x4 > 1,按照1进行对齐,x4占用11,12,13,14;
x5 > 1,按照1进行对齐,x5占用15;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes > 1byte,所以整体按照1进行对齐;16%1=0。
所以,在#pragma pack(1) 的情况下,struct Test占用内存为16bytes。

 

#pragma pack(2) // 设定对齐系数为2
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:

x1 <= 2,按照1进行对齐,x1占用0;
x2 > 2,按照2进行对齐,x2占用2,3,4,5,6,7,8,9;
x3 >= 2,按照2进行对齐,x3占用10,11;
x4 > 2,按照2进行对齐,x4占用12,13,14,15;
x5 < 2,按照1进行对齐,x5占用16;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes > 2byte,所以整体按照2进行对齐;17%2!=0
所以,在#pragma pack(2) 的情况下,struct Test占用内存为18bytes。

 

#pragma pack(4) // 设定对齐系数为4
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:

x1 <= 4,按照1进行对齐,x1占用0;
x2 > 4,按照4进行对齐,x2占用4,5,6,7,8,9,10,11;
x3 < 4,按照2进行对齐,x3占用12,13;
x4 >= 4,按照4进行对齐,x4占用16,17,18,19;
x5 < 4,按照1进行对齐,x5占用20;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes > 4byte,所以整体按照4进行对齐;21%4!=0
所以,在#pragma pack(4) 的情况下,struct Test占用内存为24bytes。

 

#pragma pack(8) // 设定对齐系数为8
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:

x1 <= 8,按照1进行对齐,x1占用0;
x2 >= 8,按照8进行对齐,x2占用8,9,10,11,12,13,14,15;
x3 < 8,按照2进行对齐,x3占用16,17;
x4 <= 8,按照4进行对齐,x4占用20,21,22,23;
x5 < 8,按照1进行对齐,x5占用24;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes >= 8byte,所以整体按照8进行对齐;25%8!=0
所以,在#pragma pack(8) 的情况下,struct Test占用内存为32bytes。

 

#pragma pack(16) // 设定对齐系数为16
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:

x1 < 16,按照1进行对齐,x1占用0;
x2 < 16,按照8进行对齐,x2占用8,9,10,11,12,13,14,15;
x3 < 16,按照2进行对齐,x3占用16,17;
x4 < 16,按照4进行对齐,x4占用20,21,22,23;
x5 < 16,按照1进行对齐,x5占用24;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,16bytes >= 8byte,所以整体按照8进行对齐;25%8!=0
所以,在#pragma pack(16) 的情况下,struct Test占用内存为32bytes。

 

总结

经过上面的实例分析,我对内存对齐有了全面的认识和了解。现在再回过来看看文章开头的那段代码,问题就迎刃而解了,同时经过这段代码,让我们认识到定义struct或者union时,也是有讲解的。在以后的编码生涯时,是不是又要多考虑一些呢?纠结~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C、C++内存对齐 的相关文章

  • 双缓冲列表框

    我有一个 CheckedListBox WinForms 控件 它继承自 ListBox 谷歌搜索显示问题出在 ListBox 该控件锚定到其窗体的所有四个边 当调整表单大小时 列表框会出现难看的闪烁 我尝试继承 CheckedListBo
  • 类变量在其定义范围内?

    这可能是一个愚蠢的问题 我正在尝试制作文本泥 我需要每个 Room 类包含其他 Room 类 以便在尝试移动到它们或从它们获取信息时可以引用 但是 我不能这样做 因为我显然无法在其定义中声明一个类 那么 我该怎么做呢 当我说我做不到时 我的
  • 你好世界,裸机 Beagleboard

    我正在尝试在我的 Beagleboard xm rev 上运行 hello world 类型的程序 C 通过调用 Cputs功能来自装配 到目前为止 我一直使用这个作为参考 http wiki osdev org ARM Beagleboa
  • 任务计划程序控制台输出在哪里? (C# 控制台应用程序)

    我正在运行 C Windows 控制台应用程序 并通过任务计划程序传递几个参数 它全天运行 将其他应用程序创建的平面文件数据加载到 SQL Server 中 该程序间歇性失败 并且我有 Try Catch 逻辑 该逻辑使用 Console
  • 查找周边上的点来表示边界/形状

    我有一个简单的二维网格 其格式为myGrid x y 我正在尝试找到一种方法来找到所选网格周围的周长 这样我就有了所选网格的形状 这是我的意思的一个例子 这里的想法是找到所有相关的 角点 也就是图像周边的红点 放入一个列表中 这样我就可以从
  • iPhone 编程游戏

    使用 Objective C 还是 C 为 iPhone 编写游戏最好 像 Flight Control 这样的游戏会用什么语言编写 图形应采用什么格式才能在 iPhone 上正确显示并快速加载 像 Flight Control 这样的游戏
  • C# 委托实例化与仅传递方法引用 [重复]

    这个问题在这里已经有答案了 我有一个简单的问题 与仅传递函数引用相比 实例化 C 委托有什么优势 我的意思是 Why do Thread t new Thread new ThreadStart SomeObject SomeMethod
  • 终止以 System.Diagnostic.Process.Start("FileName") 启动的进程

    我正在尝试创建一个将在特定时间执行操作的应用程序 很像 Windows 任务计划程序 我当前正在使用 Process Start 来启动任务所需的文件 或 exe 我通过调用文件 mp3 启动一个进程 该进程启动 WMP 因为它是默认应用程
  • 使用 boost::asio 是否有一种可移植的方法来查找空闲端口号

    我目前正在尝试找出一种方法来查找空闲端口号以建立连接 最好使用 boost asio 然后 该端口号将用于侦听 只有这样我才能打开套接字 大致来说 有没有办法做到 tcp resolver query query localhost por
  • C# - 如何从 Steam 交易 API 获取图标的 URL(编码)

    以下是 API 为每个项目返回的 XML 部分
  • flowlayoutpanel和水平滚动条问题

    我正在使用一个 flowlayoutpanel 它有很多逻辑上的按钮 我遇到的问题是 当我调整窗口大小时 当窗口变小时 我无法看到所有水平排列的按钮 相反 当窗口变小时 按钮会下降到下一行 谁能帮我解决这个问题 我只是希望按钮水平排列 当窗
  • 在 C++ 中从另一个数组初始化结构内的数组[关闭]

    这个问题不太可能对任何未来的访客有帮助 它只与一个较小的地理区域 一个特定的时间点或一个非常狭窄的情况相关 通常不适用于全世界的互联网受众 为了帮助使这个问题更广泛地适用 访问帮助中心 help reopen questions struc
  • 驱蚊程序?

    不 我认真的 最近 我读到 当电脑的压电蜂鸣器以一定频率振动时 声音可以驱赶蚊子 真的吗 如何以编程方式访问 PC 蜂鸣器 而不是扬声器 最好使用 C 我不知道有没有蚊子 但我的头疼得要命 啊啊 using System Runtime I
  • 在 .NET Core 上通过 MEF 将参数传递给插件构造函数?

    我花了几个小时试图弄清楚如何通过 MEF System Composition 将参数传递给插件构造函数 但一切都无济于事 不用说 相关文档很少 查看源代码也没有帮助 这曾经非常容易做到 使用 CompositionHost Compose
  • 需要帮助将 winform 迁移到 net 5

    我正在将 winform 应用程序从 net core 3 1 移植到 net 5 并收到以下错误 严重性代码 说明 项目文件行抑制状态 错误NETSDK1136 目标平台必须设置为Windows 通常 通过在 TargetFramewor
  • 使用 openssl 库获取 x509 证书哈希

    我目前正在开发一个应用程序 它使用 openssl 库 libcrypto 来生成证书 现在我必须获取现有证书的哈希值 当我使用终端时 我可以使用以下命令生成哈希值 openssl x509 hash in cert pem noout 输
  • 创建 PING 程序时限制 ICMP 回显答复

    我正在编写一个多线程 ping 程序 我在每个线程 针对每个 IP 上创建了原始套接字 并使用 sendto 向每个线程发送了 ICMP Echo 请求 然后在每个线程中执行了 receivevfrom 我正在从各种套接字中的 IP 获取消
  • 从资源文件获取 DisplayName [重复]

    这个问题在这里已经有答案了 我在 App GlobalResources 文件夹中有特定于文化的资源文件 现在我需要从此资源文件中读取 DisplayName 属性的值 我在用 Display Name MerchantName Resou
  • 警告从 lambda 返回捕获的引用

    我尝试使用 lambda 有条件地将引用绑定到两个变量之一 int foo bar int choice gt int if true some condition return foo else return bar 这会在 clang
  • 文件按文件名模式存在

    我在用 File Exists filepath 我想做的是将其替换为模式 因为文件名的第一部分发生了变化 例如 该文件可以是 01 peach xml 02 peach xml 03 peach xml 如何根据某种搜索模式检查文件是否存

随机推荐

  • LVM扩容操作

    文章目录 一 测试环境 二 给lvm分区扩容 加硬盘 1 新增硬盘 2 给新的硬盘分区 3 Lvm操作 查看卷组状态 vgdisplay 创建物理卷 pvcreate dev sdb1 扩展卷组 vgextend 卷组名 物理卷路径 扩展逻
  • 为何程序员要考教师资格证?备考指南与职业价值

    大家好 我是苍何 一个刚拿完教师资格证的非主流程序员 我考教资完全是在两年前受到一位朋友的影响 我们姑且叫他小 y 小 y 是计算机科班名校毕业 985 大学硕士学历并在华为担任软件开发工程师 这样强的专业和大厂背景 已经吊打很多程序猿了
  • 升降压电路Charger&Boost 自己的小解读

    上图为charger内部大致的结构图 Vbus进来 Q1可先认为是二极管 当Q2开启时 可以向后级电感储能 当Q2关断时 Q3 SW 电池形成一个回路 因为同样接地 电感给电池充电 锂电池标称值为3 7V 满电电压是4 2V VBUS电压是
  • LPDDR4特点和基本概念--基于Hynix H9HCNNNBPUMLHR系列

    Feature 两个Channel 每个Channel有8个Bank 对于command和address 采用SDR传输减少总引脚数量 所有的command和address在CLK上升沿锁存 每两个时钟周期传输一个command 对于数据线
  • Oracle --------序列

    1 思考问题 在某张表中 存在一个id列 整数 用户希望在添加记录的时候 该列从1开始 自动的增长 如何处理 2 介绍 Oracle通过序列处理自动增长列 1 可以为表中的列自动产生值 2 由用户创建数据库对象 并可由多个用户共享 3 一般
  • 六. go 高性能编程之 空结构体 struct{} 的使用

    目录 空结构体的优点 实现集合Set 不发送数据的信道channel 仅包含方法的结构体 空结构体的优点 因为空结构体不占据内存空间 因此被广泛作为各种场景下的占位符使用 Go 语言中 可以使用 unsafe Sizeof 计算出一个数据类
  • DDR5内存条容量计算

    DDR5内存条容量计算 一 理解DDR5通道的变化 二 理解芯片package 三 DDR5 symmetric module容量计算 之前对DDR的一些基础知识进行了总结 最近需要了解DDR5的知识 在之前文章基础上又有一些认识 所以重新
  • NumPy 的随机采样模块 random 使用简单介绍

    NumPy的API的简单介绍 NumPy 提供的random模块 提供了方便的自动生成 伪 随机数的API 一 使用简单随机数API生成随机数组 1 random rand d0 d1 dn 参数 d0 d1 dn int optional
  • MySQL 存储函数

    文章目录 1 简介 2 创建存储函数 3 调用存储函数 4 查看存储函数 SHOW FUNCTION STATUS SHOW CREATE FUNCTION 5 修改存储函数 6 删除存储函数 参考文献 1 简介 MySQL 存储函数 St
  • java向多线程中传递参数的三种方法详细介绍

    在传统的同步开发模式下 当我们调用一个函数时 通过这个函数的参数将数据传入 并通过这个函数的返回值来返回最终的计算结果 但在多线程的异步开发模式下 数据的传递和返回和同步开发模式有很大的区别 由于线程的运行和结束是不可预料的 因此 在传递和
  • 3-4 数据变换

    3 4 数据变换 请参考 数据准备和特征工程 中的相关章节 调试如下代码 基础知识 import pandas as pd data pd read csv home aistudio data data20514 freefall csv
  • Python 魔法方法

    视频版教程 Python3零基础7天入门实战视频教程 Python的魔法方法 也称为特殊方法或双下划线方法 是一种特殊的方法 用于在类中实现一些特殊的功能 这些方法的名称始终以双下划线开头和结尾 例如 init repr add 等 str
  • SaaS “可配置”和“多租户”架构的几种技术实现方式

    1 数据存储方式的选择 多租户 Multi Tenant 即多个租户共用一个实例 租户的数据既有隔离又有共享 说到底是要解决数据存储的问题 常用的数据存储方式有三种 方案一 独立数据库 一个Tenant 一个Database 的数据存储方式
  • 人脸识别体征提取arcFace技术

  • hexo编写博客问题

    创建并部署博客的基本步骤 hexo new 创建一个名字为 的博客 具体title也可在文档中修改 hexo clean 清除缓存文件 db json 和已生成的静态文件 public hexo g hexo generate的缩写 生成网
  • java中判断一个数是否为偶数

    package Day3 import com sun java swing plaf windows WindowsTabbedPaneUI public class Day03 Tast04 public static void mai
  • C#的数据类型

    C 的数据类型 1 整数类型 sbyte System SByte 8位有符号整数 short System Int16 16位有符号整数 int System Int32 32位有符号整数 long System Int64 64位有符号
  • 深度学习与计算机视觉(11)_基于deep learning的快速图像检索系统

    作者 寒小阳 时间 2016年3月 出处 http blog csdn net han xiaoyang article details 50856583 声明 版权所有 转载请联系作者并注明出处 1 引言 本系统是基于CVPR2015的论
  • openwrt 的常用指令

    https wiki openwrt org zh cn doc howto user beginner cli
  • C、C++内存对齐

    文章转载自 http www jellythink com archives 413 include