内存对齐详解 (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 设计在处理非对齐访问时进行了很多优化,因此在某些情况下,非对齐访问的性能损失可能不太明显。
标签:缓存,字节,C++,访问,详解,内存,对齐,处理器 From: https://www.cnblogs.com/Last--Whisper/p/17321538.html扩展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
类型的值,表示给定类型的对齐要求(以字节为单位)。