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

Posted

技术标签:

【中文标题】为啥动态分配的内存总是 16 字节对齐?【英文标题】:Why is dynamically allocated memory always 16 bytes aligned?为什么动态分配的内存总是 16 字节对齐? 【发布时间】:2020-03-24 15:57:42 【问题描述】:

我写了一个简单的例子:

#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;

运行示例,我得到以下结果:

字节1:0x1f53e70

字节2:0x1f53e90

字节3:0x1f53eb0

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

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

【问题讨论】:

@MosheRabaev 据我所知,alignas 用于特定的变量或类型。如何为每个对象设置默认的alignas @MosheRabaev 如果有默认对齐方式,它是否也适用于堆栈上的对象? 没有全局的alignas,不知道@MosheRabaev 想用评论说什么。 我不知道为什么默认它对齐到 16 个字节。我措辞错误,我的意思是使用 alignas 进行自定义行为。 【参考方案1】:

为什么会这样?

因为标准是这样说的。更具体地说,它表示动态分配1 至少与最大基本2 对齐(它可能具有更严格的对齐)对齐。有一个预定义的宏(从 C++17 开始)只是为了准确地告诉您这种保证对齐是什么:__STDCPP_DEFAULT_NEW_ALIGNMENT__。为什么在您的示例中这可能是 16...这是语言实现的选择,受目标硬件架构所允许的限制。

这是(曾经)必要的设计,考虑到(曾经)没有办法将有关所需对齐的信息传递给分配函数(直到 C++17 引入了对齐新语法以分配 "过度对齐的”内存)。

malloc 对您打算在内存中创建的对象类型一无所知。有人可能会认为new 理论上可以推断出对齐,因为它被赋予了一种类型……但是如果您想将该内存重新用于具有更严格对齐的其他对象,例如在实现std::vector 时怎么办?并且一旦您知道 operator new:void* operator new ( std::size_t count ) 的 API,您可以看到类型或其对齐方式不是可能影响分配对齐方式的参数。

1由默认分配器或malloc系列函数生成。

2 最大基本对齐是alignof(std::max_align_t)。没有比这更严格的对齐方式(算术类型、指针)的基本类型了。

【讨论】:

C++11中有__STDCPP_DEFAULT_NEW_ALIGNMENT__的同义词吗? 根据你的解释,__STDCPP_DEFAULT_NEW_ALIGNMENT__是16,和我在gcc 7.4 with C++17的测试结果一致。但我发现sizeof(std::max_align_t) 的值在 gcc 5.4 和 C++11 和 gcc 7.4 和 C++17 中是 32。 @jinge 很有趣。那么我可能对他们的关系有误会。我认为 STDCPP_DEFAULT_NEW_ALIGNMENT 会更大。 @eerorika 由于 C++17 [new.delete.single]/1 表示 operator new 的这种重载只需要返回一个指针,该指针适合给定大小的任何完整对象类型对齐,因为它没有new-extended 对齐方式,其中 new-extended 表示大于__STDCPP_DEFAULT_NEW_ALIGNMENT__。我没有发现任何要求它至少与最大的基本对齐一样大,即alignof(std​::​max_­align_­t)(我认为你混淆了sizeofalignof。)。跨度> @jinge 尝试使用alignof(std::max_align_t) 而不是sizeof(std::max_align_t),您将得到与__STDCPP_DEFAULT_NEW_ALIGNMENT__ 相同的结果。正如我在上面的 cmets 中提到的,这可能是 eerorika 的一个错误,但正如我也提到的,我认为这两个值不需要以某种方式排序(虽然我不确定。) 【参考方案2】:

不是。这取决于操作系统/CPU 要求。在 32bit 版本的 linux/win32 的情况下,分配的内存总是 8 字节对齐的。对于 linux/win32 的 64 位版本,由于所有 64 位 CPU 都至少具有 SSE2,因此将所有内存对齐到 16 字节是有道理的(因为使用未对齐的内存时使用 SSE2 效率较低)。使用最新的基于 AVX 的 CPU,未对齐内存的这种性能损失已被消除,因此它们实际上可以在任何边界上进行分配。

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

归根结底,推理完全取决于操作系统和/或硬件要求。这与语言无关。

【讨论】:

“将内存分配的地址对齐为 16 字节会在指针地址中为您提供 4 位空白空间”这不是原因。主要原因 - 存储在该内存中的未对齐数据的惩罚。 这句话是什么意思? “将内存分配的地址对齐到 16 字节会在指针地址中为您提供 4 位空白空间” @jinge 先验知道所有地址都将对齐意味着地址的某些位中恰好有零信息。这些位在存储值中实际上是“未使用”的,并且可以归因于其他东西,例如位域。 使用 AVX 的缓存行拆分仍然较慢,只有缓存行内的未对齐在 Intel CPU 上是免费的。一些带有 AVX 的 AMD CPU 确实关心比 64B 更窄的边界。更准确地说,AVX 允许在运行时对齐的常见情况下免费使用具有未对齐功能的指令。 (实际上 Nehalem 是这样做的,让 movups 便宜,但 AVX 允许将负载折叠到内存源操作数中,因为 VEX 编码的版本不需要对齐。) 对齐要求的真正来源是 ABI,它是为当时的 ISA 硬件设计的(例如,2000 年代初的 x86-64 System V ABI 具有 alignof(max_align_t) = 16【参考方案3】:

为什么会这样?

因为通常情况下库不知道您要在该内存中存储什么样的数据,所以它必须与该平台上最大的数据类型对齐。如果您存储未对齐的数据,您将受到硬件性能的严重影响。在某些平台上,如果您尝试访问未对齐的数据,甚至会出现段错误。

【讨论】:

在其他平台上,您甚至可能读/写错误的数据,因为 CPU 只是忽略了地址的最后几位......(这甚至比 SEGFAULT 更糟糕,恕我直言。) @cmaster 在某些情况下,不正确的地址甚至会被解码为正确地址的 one 字上的移位指令。那就是你得到一个差异结果,没有错误指示。【参考方案4】:

由于平台。在 X86 上它不是必需的,但可以获得操作的性能。正如我所知,在较新的模型上它没有什么区别,但编译器会达到最佳效果。当未正确对齐时,例如 m68k 处理器上的长未对齐 4 字节将崩溃。

【讨论】:

这里有一些测试:lemire.me/blog/2012/05/31/… 此外,对齐使内存分配器更通用且更高效。对于可能需要对齐的任何内容,它始终返回正确对齐的值,并且在内部始终是保持对齐所需大小的一些倍数。 “现在内存很充裕。”【参考方案5】:

这可能是内存分配器设法为释放函数获取必要信息的方式:释放函数的问题(如free 或一般的全局operator delete)是只有一个参数,指向分配内存的指针,并且没有指示所请求的块的大小(或者如果它更大,则分配的大小),因此需要在某些文件中提供指示(以及更多)释放函数的其他形式。

最简单而有效的方法是为附加信息加上请求的字节分配空间,并返回一个指向信息块末尾的指针,我们称之为IBIB 的大小和对齐方式会自动对齐mallocoperator new 返回的地址,即使您分配的金额很小:malloc(s) 分配的实际金额是sizeof(IB)+s

对于如此小的分配,该方法相对浪费,并且可能会使用其他策略,但是具有多种分配方法会使释放复杂化,因为函数必须首先确定使用哪种方法。

【讨论】:

【参考方案6】:

实际上有两个原因。第一个原因是,某些类型的对象有一些对齐要求。通常,这些对齐要求是软的:未对齐的访问“只是”更慢(可能是数量级)。它们也可能很难:例如,在 PPC 上,如果该向量未与 16 字节对齐,则您根本无法访问内存中的向量。 对齐不是可选的,它是分配内存时必须考虑的。总是。

请注意,无法指定与malloc() 的对齐方式。根本没有任何论据。因此,必须实现 malloc() 以提供针对平台上的任何目的正确对齐的指针。 C++中的::operator new()遵循同样的原则。

需要多少对齐完全取决于平台。在 PPC 上,您无法以少于 16 字节的对齐方式逃脱。 X86 在这方面稍微宽松一点,afaik。


第二个原因是分配器函数的内部工作原理。典型的实现具有至少 2 个指针的分配器开销:每当您从 malloc() 请求一个字节时,它通常需要为至少两个额外的指针分配空间以进行自己的记账(具体数量取决于实现)。在 64 位架构上,这是 16 个字节。因此,malloc() 用字节来思考是不明智的,用 16 字节块来思考会更有效。至少。您可以通过示例代码看到这一点:生成的指针实际上相隔 32 个字节。每个内存块占用 16 字节有效载荷 + 16 字节内部簿记内存。

由于分配器从内核请求整个内存页面(4096 字节,4096 字节对齐!),因此在 64 位平台上,生成的内存块自然是 16 字节对齐的。 提供较少对齐的内存分配根本不切实际。


因此,综合考虑这两个原因,从分配器函数提供严格对齐的内存块既实用又需要。确切的对齐量取决于平台,但通常不会小于两个指针的大小。

【讨论】:

以上是关于为啥动态分配的内存总是 16 字节对齐?的主要内容,如果未能解决你的问题,请参考以下文章

C++11 中的动态对齐内存分配

4096个字节是4K对齐了吗?

为啥 dlmalloc 分配的块头包含 4 个字节的先前分配的块 [关闭]

为啥我们不能在堆栈上分配动态内存?

分配粒度和内存页面大小(x86处理器平台的分配粒度是64K,内存页是4K,所以section都是0x1000对齐,硬盘扇区大小是512字节,所以PE文件默认文件对齐是0x200)

为啥在使用 realloc() 进行动态内存分配之后再添加一块内存?