为什么动态分配的内存总是16字节对齐?

7

我写了一个简单的例子:

#include <iostream>

int main() {
    void* byte1 = ::operator new(1);
    void* byte2 = ::operator new(1);
    void* byte3 = malloc(1);
    std::cout << "byte1: " << byte1 << std::endl;
    std::cout << "byte2: " << byte2 << std::endl;
    std::cout << "byte3: " << byte3 << std::endl;
    return 0;
}

运行代码后,我得到了以下结果:

byte1: 0x1f53e70

byte2: 0x1f53e90

byte3: 0x1f53eb0

每次我分配一个单字节的内存时,它总是以16字节对齐。这是为什么呢?

我在GCC 5.4.0和GCC 7.4.0上测试了这段代码,并得到了相同的结果。


1
据我所知,alignas是针对特定变量或类型使用的。我如何将默认的alignas设置为每个对象? - jinge
@MosheRabaev 如果有默认对齐方式,那么它是否也适用于堆栈上的对象? - curiousguy
没有全局的alignas,我不知道@MosheRabaev在评论中想表达什么意思。 - walnut
我不知道为什么默认情况下它会对齐到16个字节。我的措辞有误,我的意思是使用alignas来实现自定义行为。 - Moshe Rabaev
6个回答

5
为什么会发生这种情况?
因为标准规定了如此。更具体地说,它规定动态分配至少要对齐到最大基本对齐度(可能有更严格的对齐度)。自C++17以来,有一个预定义的宏专门用于告诉您确切的保证对齐度:__STDCPP_DEFAULT_NEW_ALIGNMENT__。在您的示例中为什么会是16 ... 这是语言实现的选择,受目标硬件体系结构所允许的限制。
鉴于(曾经)没有办法将关于所需对齐方式的信息传递给分配函数(直到引入用于分配“超对齐”内存的aligned-new语法),这是(曾经)必要的设计。
malloc不知道您打算创建到内存中的对象的类型。有人可能认为new理论上可以推断出对齐方式,因为它给出了一种类型...但是如果您想要重用该内存以供具有更严格对齐的其他对象,例如在实现std::vector时,该怎么办呢?一旦了解了operator new的API:void* operator new (std::size_t count),您就可以看到类型或其对齐方式不是可能影响分配对齐的参数。
1由默认分配器或malloc系列函数创建。
2最大基本对齐度为alignof(std::max_align_t)。没有基本类型(算术类型,指针)比这更严格的对齐方式。

根据您的解释,__STDCPP_DEFAULT_NEW_ALIGNMENT__ 是16,这与我在使用C++17的gcc 7.4中的测试结果一致。但是我发现,在使用C++11的gcc 5.4和C++17的gcc 7.4中,sizeof(std::max_align_t) 的值为32。 - jinge
@eerorika 自 C++17 [new.delete.single]/1 规定,只要 operator new 的重载返回一个适当对齐的指针,以满足给定大小的任何完整对象类型,前提是它没有 new-extended 对齐方式,其中 new-extended 意味着大于 __STDCPP_DEFAULT_NEW_ALIGNMENT__。我没有找到任何要求这个对齐方式至少与最大的 基本对齐方式 一样大的内容,而最大的 基本对齐方式alignof(std::max_align_t)(我认为您混淆了 sizeofalignof)。 - walnut
在 C++17 之前,这个 operator new 的重载必须返回一个适合于任何具有基本对齐的对象的指针,但如果我理解正确的话,自 C++17 以来,可能会存在介于对齐的情况,这时就需要通过使用 operator new(std::size_t, std::align_val_t) 重载来处理。 - walnut
请参阅C++17中的基本对齐,了解extendednew-extended对齐方式的定义。 - walnut
2
@jinge 请尝试使用alignof(std :: max_align_t)而不是sizeof(std :: max_align_t),您将获得与__STDCPP_DEFAULT_NEW_ALIGNMENT__相同的结果。如我在上面的评论中提到的那样,这可能是eerorika的错误,但是正如我也提到的,我认为这两个值不需要按特定方式排序(虽然我不能确定)。 - walnut
显示剩余2条评论

5
实际上有两个原因。第一个原因是某些对象需要一些对齐要求。通常,这些对齐要求是软性的:未对齐的访问“只是”较慢(可能比顺序慢得多)。它们也可以是硬性的:例如,在 PPC 上,如果该向量未对齐到 16 字节,则根本无法访问内存中的向量。对齐不是可选项,而是在分配内存时必须考虑的内容。始终如此。 请注意,没有办法为 malloc() 指定对齐方式。根本没有参数。因此,malloc() 必须实现为提供在平台上任何目的下正确对齐的指针。C++ 中的 ::operator new() 遵循相同的原则。
所需的对齐方式完全取决于平台。在 PPC 上,您无法以小于 16 字节的对齐方式运行。据我所知,X86 对此更加宽容。
第二个原因是分配器函数的内部工作方式。典型的实现至少具有 2 个指针的分配器开销:每当您从 malloc() 请求一个字节时,它通常需要为至少两个额外的指针分配空间以进行自己的簿记(确切的数量取决于实现)。在 64 位架构上,这是 16 字节。因此,malloc() 不应该考虑字节,而是更有效地考虑 16 字节块。至少如此。您可以通过示例代码看到这一点:生成的指针实际上相距 32 字节。每个内存块占用 16 字节的有效载荷 + 16 字节的内部簿记内存。
由于分配器从内核请求整个内存页面(4096 字节,4096 字节对齐!),因此在 64 位平台上,生成的内存块自然是以 16 字节对齐的。提供不太对齐的内存分配根本不现实。
因此,考虑到这两个原因,从分配器函数提供严格对齐的内存块既实用又必要。对齐的确切数量取决于平台,但通常不会小于两个指针的大小。

2
这可能与内存分配器管理将必要信息传递给释放函数的方式有关:释放函数(如free或通用的全局operator delete)的问题在于,只有一个参数,即指向分配内存的指针,并没有指示请求的块大小(或者如果分配的更大,则已分配的大小),因此需要以其他形式提供该指示(以及更多信息)给释放函数。
最简单但高效的方法是为该附加信息和所请求的字节数分配空间,并返回指向信息块末尾的指针,称之为IB。 IB的大小和对齐方式自动对齐由malloc或operator new返回的地址,即使您分配了微小的金额:malloc(s)实际分配的金额是sizeof(IB)+s。
对于这样的小型分配,该方法相对浪费,可以使用其他策略,但是拥有多个分配方法会使释放变得复杂,因为函数必须首先确定使用了哪种方法。

0
为什么会发生这种情况?
因为通常情况下,库不知道您将在该内存中存储什么类型的数据,因此必须对其进行与该平台上最大数据类型对齐。如果您存储未对齐的数据,则硬件性能将受到显着惩罚。在某些平台上,如果尝试访问未对齐的数据,甚至会导致段错误。

1
在其他平台上,您甚至可能读/写错误的数据,因为CPU简单地忽略了地址的最后几位...(在我看来,这比SEGFAULT还要糟糕。) - cmaster - reinstate monica
2
在某些情况下,一个错误的地址甚至被解码为正确地址上的一个字的移位指令。这意味着你会得到一个不同的结果,而没有错误提示。 - curiousguy

0
由于平台的原因,在X86上并不需要,但可以提高操作性能。据我所知,在较新的型号上没有区别,但编译器会选择最佳选项。如果未正确对齐,例如在m68k处理器上未对齐的长4字节将导致崩溃。

以下是一些测试:https://lemire.me/blog/2012/05/31/data-alignment-for-speed-myth-or-reality/ - rifkin
此外,对齐使得内存分配器更具通用性和稍微更有效率。 它总是返回适用于可能需要对齐的任何东西的正确对齐值,并且总是在内部以维护该对齐所需大小的某个倍数为基础。 “现在内存很充裕。” - Mike Robinson

-1

这取决于操作系统/CPU的要求。在32位版本的linux/win32中,分配的内存总是按8字节对齐的。在64位版本的linux/win32 中,由于所有64位CPU至少都有SSE2,所以在当时将所有内存对齐到16字节是有意义的(因为使用未对齐内存时,使用SSE2的效率较低)。随着最新的基于AVX的CPU,未对齐内存的性能惩罚已被消除,因此它们可以在任何边界上进行分配。

如果你这样考虑,将用于内存分配的地址对齐到16字节会给指针地址留下4位空间。这可能在内部存储一些附加标志(例如可读、可写、可执行等)方面很有用。

归根结底,推理完全受到操作系统和/或硬件要求的支配。这与语言无关。


1
将内存分配的地址对齐到16字节,可以在指针地址中获得4位空白空间,但这不是原因。主要原因是未对齐的数据存储在该内存中会受到惩罚。 - Slava
这句话是什么意思?“将内存分配的地址对齐到16字节,可以在指针地址中获得4位空白空间。” - jinge
1
@jinge 事先知道所有地址都将对齐意味着某些位中确切地没有任何信息。这些位在存储的值中实际上是“未使用”的,可以像位域一样归属于其他东西。 - curiousguy
使用AVX仍然会导致缓存行分裂,只有在Intel CPU中缓存行内的不对齐才是免费的。一些支持AVX的AMD CPU确实关心小于64B的边界。更准确地说,AVX使得在运行时它们实际上是对齐的常见情况下,可以免费使用不对齐能力指令。 (实际上,Nehalem做到了这一点,使movups变得便宜,但AVX允许将负载折叠到内存源操作数中,因为VEX编码版本不需要对齐。) - Peter Cordes
对齐要求的真正来源是ABI,它是为当时ISA的硬件设计的(例如,x86-64 System V ABI早期的2000年代,其alignof(max_align_t) = 16)。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接