内存对齐详解 (C++代码)
Posted Last_Whisper
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内存对齐详解 (C++代码)相关的知识,希望对你有一定的参考价值。
内存对齐详解 (C++代码)
我每天都有读一下面经的习惯,从里面抽一些我不太懂的内容使用 dfs
的思想去探索,感谢有了GPT4,探索之路变得有趣和高效许多。
今天这个 Topic 如下,是腾讯日常实习面经中的内容
编译内存相关:
- C++内存管理,分了多少段,在堆上和在栈上的区别,为什么要区分堆和栈,new和malloc的区别,
- 内存对齐,为什么会出现内存对齐问题,程序员为什么要关注内存对齐问题
内存对齐是指数据在内存中的存储方式,要求数据存储在特定边界的倍数上。例如,4 字节的数据类型应该存储在 4 的倍数的地址上。内存对齐主要出于以下原因:
-
性能优化:对齐内存可以减少访问内存所需的 CPU 周期数。现代计算机硬件对于对齐的内存访问通常更加高效。不对齐的内存访问可能需要多次访问来读取或写入一个变量,而对齐的内存只需要一次访问。这里涉及缓存行的知识,后面相对提了提)
-
硬件要求:某些硬件平台要求数据访问必须对齐。否则,这可能导致硬件异常、程序崩溃或其他未定义行为。
程序员需要关注内存对齐问题,因为它可能影响程序的性能和稳定性。以下是关于内存对齐的详细讨论:
-
对齐边界:对齐边界通常是数据类型大小的倍数。例如,2 字节的数据类型(如 short)应该对齐到 2 字节边界,4 字节的数据类型(如 int)应该对齐到 4 字节边界,依此类推。
-
结构体对齐:结构体中的成员变量可能会导致内存对齐问题。编译器通常会自动对齐结构体成员,但这可能导致结构体大小增加,因为编译器会在成员之间插入填充字节。了解如何手动调整结构体成员顺序和对齐,以减少填充和提高性能,对程序员来说很有帮助。后面我们通过代码展示结构体中的内存对齐
-
编译器对齐:大多数编译器都提供了对齐设置的选项,可以在编译时指定对齐要求。例如,在 GCC 编译器中,可以使用
__attribute__((aligned(x)))/std::alignas(x)
指定对齐边界。后续我们会讲解这些关键词 -
内存分配对齐:动态分配内存时,需要确保分配的内存满足对齐要求。许多内存分配函数,如 C++ 中的
new
或 C 中的malloc
,通常会自动对齐内存。但在某些情况下,需要手动处理对齐问题,例如使用特殊的内存分配函数。
总之,内存对齐对于程序性能和稳定性很重要。了解内存对齐原理和如何处理内存对齐问题,有助于编写高效、稳定的程序。
Test1: 结构体对齐
当我们说 "4 字节的数据类型应该存储在 4 的倍数的地址上" 时,我们是指数据在内存中的存储位置应该是 4 的整数倍。这是为了确保更高效的内存访问和满足某些硬件平台的要求。
假设我们有一个 4 字节的整数类型(如 int
),它的内存地址应该是 4 的倍数,比如 0x1000、0x1004、0x1008 等。
#include <bits/stdc++.h>
struct MyStruct
char a;
int b;
char c;
;
int main()
MyStruct my_struct;
uintptr_t a_address = reinterpret_cast<uintptr_t> (&my_struct.a);
uintptr_t b_address = reinterpret_cast<uintptr_t> (&my_struct.b);
uintptr_t c_address = reinterpret_cast<uintptr_t> (&my_struct.c);
std::cout << "Address of a: " << std::hex << std::showbase << a_address << std::endl;
std::cout << "Address of b: " << std::hex << std::showbase << b_address << std::endl;
std::cout << "Address of c: " << std::hex << std::showbase << c_address << std::endl;
return 0;
Address of a: 0x7ff7b762b530
Address of b: 0x7ff7b762b534
Address of c: 0x7ff7b762b538
在这个例子中,我们定义了一个名为 MyStruct
的结构体,包含一个 char
类型的成员 a
(1 字节),一个 int
类型的成员 b
(4 字节),和另一个 char
类型的成员 c
(1 字节)。
当我们创建一个 MyStruct
类型的实例并输出每个成员的地址时,你可能会注意到,尽管 a
和 c
的地址可能不是 4 的倍数,但 b
的地址通常是 4 的倍数(按理来说,char
之后的 b
地址应该不在 4 的倍数上)。这是因为编译器会自动处理内存对齐,以确保 4 字节的 int
类型成员 b
存储在一个 4 的倍数的地址上,从而提高内存访问效率。这个过程可能涉及到在成员之间插入填充字节。
Q: 那存储在4的倍数的地址上有什么好处呢?
存储在 4 的倍数的地址上对于 4 字节的数据类型(如
int
)具有好处,主要是因为对齐内存访问可以提高处理器性能。以下是更详细的说明:
处理器访问内存时,通常会一次性加载多个字节(称为一个“字”或“字宽”),例如,一次加载 4 个字节。处理器的内存总线宽度通常与其字宽相匹配。如果数据已经对齐,处理器只需要一次内存访问就可以将整个数据值加载到寄存器中。
举例说明:假设处理器的字宽为 4 字节(32 位),并且有一个 4 字节的整数存储在地址 0x1004(4 的倍数)。处理器可以一次访问将整个整数加载到寄存器中。因为内存对齐,处理器不需要执行额外的内存访问。
如果数据没有对齐,处理器可能需要执行多次内存访问才能获取完整的数据值。这会导致额外的性能开销。
举例说明:假设处理器的字宽为 4 字节(32 位),但一个 4 字节的整数存储在地址 0x1005(不是 4 的倍数)。这种情况下,处理器需要两次内存访问才能获取整个整数值。首先,处理器需要从地址 0x1004 加载 4 个字节,然后从地址 0x1008 加载另外 4 个字节。接下来,处理器需要将这两部分组合成一个完整的整数值。这样,处理器需要执行额外的内存访问和数据组合操作,导致性能降低。
因此,将数据存储在其大小的倍数的地址上(如将 4 字节的数据类型存储在 4 的倍数的地址上)有助于提高处理器性能,因为处理器可以更有效地访问内存。对于现代处理器和硬件平台来说,这种性能优化是非常重要的,因为内存访问延迟通常是处理器性能的关键瓶颈。
Q: 那为什么我们不能从 0x1005 开始读取内存呢,这样的话我们同样可以 一次性读完呢?
首先,我们有一个前置知识,缓存行(可以参考“伪共享问题”的理解)。处理器通常一次访问一个固定大小的内存块,称为“缓存行”或“缓存块”。处理器从内存中读取数据时,会将整个缓存行加载到缓存中。然后,处理器可以在缓存中访问所需的数据。
首先,要理解缓存行的概念与数据对齐是两个独立的概念。缓存行是处理器缓存的组织方式,通常包含连续的多个字节。缓存行大小(例如 64 字节)是处理器设计的一个固定参数,它主要是为了优化内存访问性能。
而数据对齐是编程时需要考虑的概念,指的是将数据放置在与其大小相匹配的地址。数据对齐有助于提高处理器访问内存的性能。在某些处理器上,对齐访问要比非对齐访问快得多。
现在回到你的问题。缓存行本身并不关心数据的对齐情况。实际上,缓存行可以从任何地址开始,包括非对齐的地址。然而,处理器在设计时,通常会使缓存行的起始地址与缓存行大小对齐,以优化内存访问性能。因此,缓存行通常从对齐的地址开始,如 0x1000、0x1040、0x1080 等。
如果缓存行从非对齐的地址开始,如 0x1005,当处理器访问对齐的数据时,可能会跨越两个缓存行。这会导致额外的内存访问和数据组合操作,从而降低性能。通过将缓存行与其大小对齐,可以减少这种情况的发生,从而提高处理器访问内存的性能。
总之,虽然缓存行可以从任何地址开始,包括非对齐的地址,但处理器通常会使缓存行与其大小对齐,以优化内存访问性能。数据对齐是一个独立的概念,它有助于提高处理器访问内存的性能,尤其是在某些对齐访问性能更好的处理器上。
所以,最后答案是,实际上这是由处理器决定的,是依据工程经验制定的。
Test2: 时间开销对比
我们可以使用字符(char
)数组作为基础,并将其转换为 int
指针。这是一个修示例,这个例子使用 C++11 的 chrono
库进行计时,并使用 alignas
关键字(后面有这部分的语法补充解释)来强制数据对齐。请注意,这个例子的性能差异可能因处理器、编译器和系统配置的不同而有所不同。
这段代码使用一个 char
类型的缓冲区来模拟非对齐访问。aligned_data
指针指向缓冲区的开头,而 unaligned_data
指针指向缓冲区的第二个字节(即非对齐的 int
地址)。这样,你应该能看到对齐和非对齐访问之间的性能
#include <ios>
#include <iostream>
#include <chrono>
int main()
const int kSize = 100000;
const int kRepeat = 10000;
// 对齐
alignas(4) char buffer[sizeof(int) * (kSize + 1)];
int* aligned_data = reinterpret_cast<int*>(buffer);
int* unaligned_data = reinterpret_cast<int*>(buffer + 1);
// Initialize data
for (int i = 0; i < kSize; ++i)
aligned_data[i] = i;
unaligned_data[i] = i;
// 用来展示不同地址对齐
std::cout << std::hex << std::showbase << \\
"aligned_data address: " << &aligned_data[0] << "\\n" \\
"unaligned_data address: " << &unaligned_data[0] << "\\n";
// 回到标准的 std::dec
std::cout.unsetf(std::ios_base::basefield);
// Measure time for aligned access
auto start_aligned = std::chrono::high_resolution_clock::now();
for (int r = 0; r < kRepeat; ++r)
int sum = 0;
for (int i = 0; i < kSize; ++i)
sum += aligned_data[i];
auto end_aligned = std::chrono::high_resolution_clock::now();
// Measure time for unaligned access
auto start_unaligned = std::chrono::high_resolution_clock::now();
for (int r = 0; r < kRepeat; ++r)
int sum = 0;
for (int i = 0; i < kSize; ++i)
sum += unaligned_data[i];
auto end_unaligned = std::chrono::high_resolution_clock::now();
auto aligned_duration = std::chrono::duration_cast<std::chrono::microseconds>(end_aligned - start_aligned).count();
auto unaligned_duration = std::chrono::duration_cast<std::chrono::microseconds>(end_unaligned - start_unaligned).count();
std::cout << "Time for aligned access: " << aligned_duration << " microseconds" << std::endl;
std::cout << "Time for unaligned access: " << unaligned_duration << " microseconds" << std::endl;
return 0;
aligned_data address: 0x7ff7bacc8ab4
unaligned_data address: 0x7ff7bacc8ab5
Time for aligned access: 1018942 microseconds
Time for unaligned access: 1032926 microseconds
可以发现,首先我们的地址确实设计成了对齐和非对齐两种情况。
其次,我们发现确实有运行效率的差异。但是现代 CPU 设计在处理非对齐访问时进行了很多优化,因此在某些情况下,非对齐访问的性能损失可能不太明显。
扩展1:
__attribute__((aligned(x)))
和alignas(x)
的区别和使用?struct alignas(16) AlignedData int a; float b; double c; ; struct __attribute__((aligned(16))) AlignedData int a; float b; double c; ;
两种代码都是可行的。
__attribute__((aligned(x)))
和alignas(x)
都用于指定数据的对齐要求,但它们之间有一些细微的差别:
__attribute__((aligned(x)))
是 GCC 编译器的特定语法,这意味着它在 GCC 编译器以及部分兼容 GCC 的编译器中可用,例如 Clang。然而,它在其他编译器,如 Microsoft Visual Studio 中可能不受支持。alignas(x)
是 C++11 标准中引入的语法,它在遵循 C++11 或更高标准的编译器中可用。与__attribute__((aligned(x)))
不同,alignas(x)
是跨编译器可用的标准语法。所以,一般情况下我们还是使用
alignas(x)
更好。扩展2: 具体C++语法解析
对齐要求(Alignment)--- CPP reference
每种对象类型都有一种名为对齐要求(
alignment requirement
)的属性,它是一个非负整数值(类型为std::size_t
,并且始终为2的幂),表示在这种类型的对象可以被分配的连续地址之间的字节数。
- 类型的对齐要求可以使用
alignof
或std::alignment_of
查询。- 指针对齐函数
std::align
可用于在某个缓冲区中获取适当对齐的指针,std::aligned_storage
可用于获取适当对齐的存储空间。(自
C++11
起)每种对象类型都对其所有对象施加其对齐要求;可以使用alignas
(自C++11
起)请求更严格的对齐(即具有更大的对齐要求)。为了满足类的所有非静态成员的对齐要求,可能会在某些成员之后插入填充位。
具体还有
alignof
使用方法和sizeof
类似,参考。
alignof
是 C++11 引入的一个操作符,用于查询类型或对象的对齐要求。对齐要求表示该类型或对象所需的内存地址的倍数。例如,如果某个类型的对齐要求是 4,则该类型的对象应该存储在 4 的倍数的内存地址上。
alignof
操作符的语法如下:alignof(Type)
其中
Type
是要查询对齐要求的类型。
alignof
返回一个std::size_t
类型的值,表示给定类型的对齐要求(以字节为单位)。
c/c++内存对齐详解
c/c++内存对齐详解
一、为什么会有内存对齐?
进行内存对齐的作用主要有两个.
( 1 )平台移植 : 不是所有的硬件平台都能够访问任意地址上的数据,
( 2 )性能 : 内存对齐后访问速度提升了 (对于访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。)
为什么内存对齐会提升效率?
CPU把内存当成是一块一块的,块的大小可以是2、4、8、16字节等大小。CPU在读取内存的时候是一块一块读取的。块大小即memory access granularity:内存读取粒度。
假设CPU要读取一个int型4字节大小的数据,看下列2种情况:
(1)数据从0字节开始;
(2)数据从1字节开始;
假设内存读取粒度是4.情况(1)的时候,CPU只需一次便可把4字节读取出来。
但是情况(2)的时候,要复杂一些。这个时候CPU先访问一次内存,读取0-3字节进寄存器,再读取4-7字节进寄存器,然后把0、6、7、8字节的数据删除掉,最后合并1-4字节的数据。可以看出,如果内存没有对齐,所进行的操作要复杂得多。
二、对齐规则?
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
规则:
(1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
(2)结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
(3)当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
三、示例代码
#include<stdio.h>
struct test
char a;
int b;
char c;
double d;
char e;
;
int main(void)
printf("%d\\n",sizeof(struct test));
return 1;
输出结果为24
我的执行环境是Debian Linux(内核2.6.18),gcc 4.1.2,其实与在windows下使用vc的原理是一样的而且不对默认对齐进行修改的话,结果也是一样的。
四、分析
默认情况下,c/c++一般是设置对齐系数为4,对于上面的例子也是如此。从上面的结果我们可以看出结构体test占用的内存大小为24,而不是1+4+1+8+1=15,那么24是如何得到的呢?
按照二(1)中的规则对各个成员占用的内存进行分析如下:
struct test
char a; /*长度1 < 4 按1对齐;起始offset=0, 0%1=0,存放区间[0]*/
int b; /*长度4 = 4 按4对齐;起始offset=1, 4%4=0;存放位置区间[4,7] */
char c; /*长度1 < 4 按1对齐;起始offset=8, 8%1=0,存放区间[8]*/
double d;/*长度8 > 4 按4对齐;起始offset=9, 12%4=0,存放区间[12,19]*/*/
char e; /*长度1 < 4 按1对齐;起始offset=20, 20%1=0,存放区间[20]*/
;
在按照二(2)中的规则对结构体整体占用的内存进行分析:
整体对齐系数为min(对齐系数,max(成员占用内存大小))=min(4,8)=4
经过上面的分析test的成员共占用内存区间[0,20],大小为21个字节,然后进行整体对齐,需要满足整体为整体系数4的倍数,那么最近的大小就是24了,所以结构体test占用的内存空间为24字节。
五、数组,嵌套.
#include <iostream>
#include <cstdio>
using namespace std;
#pragma pack(8)
struct Args
char ch;
double d;
short st;
char rs[9];
int i;
args;
struct Argsa
char ch;
Args test;
char jd[10];
int i;
arga;
int main()
// cout <<sizeof(char)<<" "<<sizeof(double)<<" "<<sizeof(short)<<" "<<sizeof(int)<<endl;
//cout<<sizeof(long)<<" "<<sizeof(long long)<<" "<<sizeof(float)<<endl;
cout<<"Args:"<<sizeof(args)<<endl;
cout<<""<<(unsigned long)&args.i-(unsigned long)&args.rs<<endl;
cout<<"Argsa:"<<sizeof(arga)<<endl;
cout<<"Argsa(i-jd):"<<(unsigned long)&arga.i -(unsigned long)&arga.jd<<endl;
cout<<"Argsa(jd-test):"<<(unsigned long)&arga.jd-(unsigned long)&arga.test<<endl;
return 0;
输出结果:
Args:32
10
Argsa:56
Argsa(i-jd):12
Argsa(jd-test):32
#include "stdafx.h"
#include "iostream"
using namespace std;
#pragma pack(8)
struct tagOne
short s;
char cArray[4];
double d;
;
struct tagTwo
char c;
tagOne tOne;//tOne的大小为16,在tagTwo中整体以8以齐,所以上面char c补7个字节,
;
int _tmain(int argc, _TCHAR* argv[])
tagTwo temp;
printf("%d\\n",sizeof temp);
return 0;
上面这个例子的输出结果为24
当结构体中出现结构体类型的成员时,不会将嵌套的结构体类型的整体长度参与到对齐值计算中,而是以嵌套定义的结构体所使用对齐值进行对齐,
六、其它
1. 在编写代码时候可以通过#pragma pack(n),n=1,2,4,8,16来灵活控制内存对齐的系数,当需要关闭内存对齐时,可以使用#pragma pack()实现。
2. 注意事项
内存对齐可以大大的提高编译器的处理速度,但不是任何时候都是必需的,有的时候不注意的话,还可能出现意想不到的错误!最典型的情况就是网络通信程序的编码中,一定要在定义结构体或者联合之前使用#pragma pack()把内存对齐关闭,这是因为远程主机通常不知道对方使用的何种对齐方式,通过socket接收的字节流,然后按照字节解析得到对应的结果,如果使用内存对齐,远程主机很哟可能会得到错误的结果!这种情况曾经指导上机时遇到过,而且属于比较隐蔽的错误,debug了好久才发现问题出在这里。
3. 优化结构体
虽然内存对齐可以提高运行效率,但是却浪费了内存,在现代PC上通常不会在乎这点小的空间,但是在一些内存很小的嵌入式设备上,可能就要锱铢必较了。其实我们发现在不影响功能的前提下,可以调整成员的顺序来减少“内存空洞”带来的浪费。如果三.中的结构体代码可以调整为
struct test
char a;
char c;
char e;
int b;
double d;
这个时候整个结构体占用的内存空间将会从上面的24减少到16。
以上是关于内存对齐详解 (C++代码)的主要内容,如果未能解决你的问题,请参考以下文章