【问题标题】:Custom allocator and memory alignment自定义分配器和内存对齐
【发布时间】:2020-08-22 03:43:25
【问题描述】:

我正在尝试根据此处的要求实现自定义分配器以使用标准容器: https://en.cppreference.com/w/cpp/named_req/Allocator

我目前正在尝试实现一个线性分配器,但我在内存对齐方面遇到了困难。
分配一块内存后,我想知道块中每个对象之间需要多少填充以优化 cpu 读/写。 我不确定地址对齐是否应该是整除的

  • 按 cpu 字长(32 位机器上为 4 字节,64 位机器上为 8 字节)
  • sizeof(T)
  • alignof(T)

我在不同的地方读到不同的答案。
例如在this 问题中,接受的答案是:

通常的经验法则(直接来自 Intel 和 AMD 的优化) 手册)是每个数据类型都应该按自己的大小对齐。一个 int32 应该在 32 位边界上对齐,一个 int64 在 64 位上 边界,等等。 char 适合任何地方。

因此,根据该答案,地址对齐应该可以被sizeof(T)整除。

关于this 问题的第二个答案表明:

CPU 总是以其字长(在 32 位处理器上为 4 字节)读取, 因此,当您执行未对齐的地址访问时 - 在处理器上 支持它——处理器将读取多个单词。

所以根据这个答案,地址对齐应该可以被 cpu 字长整除。

所以我看到一些关于如何优化 cpu 读/写数据对齐的相互矛盾的陈述,我不确定我是否理解不正确或者有一些错误的答案?也许有人可以帮我弄清楚地址对齐应该被什么整除。

【问题讨论】:

  • 这取决于您打算将 RAM 用于什么目的,如果您要分配大块,则页面对齐可能会更好。没有一种适合所有解决方案的解决方案,这就是编译器在编译时根据类型填充结构的原因,除非另有说明。如果您想同时使用 32 位和 64 位实例,只需以浪费一些 RAM 为代价对齐到 64 位。
  • @Geoffrey 所以根据你的回答,我可以假设我应该根据 cpu 字大小而不是 sizeof(T) 对齐数据?
  • sizeof(T) 可能是 3 个字节,这取决于类型,所以是的,对齐 CPU 字长。同样,如果您要处理大块数据的副本,则与页面大小对齐会更好。
  • @Geoffrey 不仅仅是普通的对象数据。对齐也是我传递给分配器的参数,所以我可以用这种方式实例化它:m_Alignment = sizeof(T*) 不能吗?如果我对齐 4 个字节或 8 个字节(取决于 cpu 字大小),如果第一个地址可被 4/8 整除,是否总是有 0 填充?如果是这样,如果我可以保证起始地址可以被 4/8 整除,这难道不是一种优化,可以确保池中有更多对象吗?如果是这样的话,我可以用 malloc 保证这样的地址划分?

标签: c++ c++11 memory-management cpu allocator


【解决方案1】:

作为一般经验法则(也就是说,除非您有充分的理由不这样做),否则您希望将给定 C++ 类型的元素与它们的对齐方式对齐,即alignof(T)。如果该类型想要对齐到 32 位边界(如在大多数常见的 c++ 实现中实现的 int),它将表现出合适的(4 字节)对齐。

当然,在T 类型的两个不同对象的基地址之间必须至少有sizeof(T) 字节的空间,这通常是其对齐的整数值倍数(实际上很难将过度对齐的类型传递给模板函数,因为它将去除任何外部 alignas 属性)。

在大多数用例中,您可以执行以下操作:在底层存储中找到与alignof(T) 对齐的第一个基地址,然后以sizeof(T) 的步骤从那里继续前进。

这样,您将依靠分配器的用户告诉您他们想要什么。这正是您想要的,因为优化器可能依赖于有关对齐的知识,例如,为双精度浮点数组发出 SSE 对齐负载,如果它们对齐错误,这将导致您的程序崩溃。

进入兔子洞

这会产生以下可能的情况:

  1. 简单类型,具有字长和字对齐(例如,intsizeof(int) = 4alignof(int) = 4):
sizeof(T) = 4 and alignof(T) = 4
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaaaaa][bbbbbbbbbb][cccccccccc][dddddddddd]
  1. 大小是对齐倍数的类型(例如,using T = int[2]
sizeof(T) = 8 and alignof(T) = 4
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaaaaaaaaaaaaaaaaa][bbbbbbbbbbbbbbbbbbbbbb]
  1. 过度对齐的类型,对齐大于大小(例如,using T = alignas(8) char[3])。 这里有龙!
sizeof(T) = 3 and alignof(T) = 8
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaa]               [bbbbbbb]

请注意,过度对齐的示例中有 未使用的空间。这是必要的,因为与 8 字节边界对齐的对象可能不会放在其他任何地方,从而导致潜在的浪费。此类类型最常见的用途是特定于 CPU 的优化,例如防止false sharing

  1. 最后,有些对象的大小大于但不是其对齐的整数倍(例如,using T = alignas(4) char[5];)。这基本上只是对前面过度对齐类型示例的一个小扩展:
sizeof(T) = 5 and alignof(T) = 4
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaaaaaaaa]         [bbbbbbbbbbbbb]

虽然对齐可以将第二个对象放置在基地址4,但那里已经有一个对象。

将所有这些示例放在一起,T 类型的两个对象的基地址之间需要的字节数是:

inline auto object_distance = sizeof(T) % alignof(T) == 0 ? sizeof(T) : sizeof(T) + (alignof(T) - sizeof(T) % alignof(T));

【讨论】:

  • 不是auto object_distance = (sizeof(T) + alignof(T) - 1) / alignof(T) * alignof(T); 吗? (实际上是ceil(sizeof(T), alignof(T))
  • 现在我用-((int)sizeof(T)) & (alignment - 1) 计算填充,它适用于您展示的所有情况。问题是alignment 的值应该是多少。现在我使用alignment = sizeof(T*) 在 4 字节/8 字节之间交替,具体取决于 cpu 字长。我这样做是否正确?
  • @MooingDuck 两个表达式都会产生相同的结果(如果我没有在某处犯错的话)。
  • @Jorayen 对于 2 的幂的对齐方式,x & (alignment - 1) 等同于 x % alignment,这应该可以帮助您了解您的表达与我所展示的表达之间的关系。 T 的对齐方式应始终为 alignof(T)
  • @gha.st:哦,我以为你的数学是对的。我只是提供了一个没有三元的变体。三进制很难阅读。其实再看一遍,你有sizeof(T) + (alignof(T) - sizeof(T) % alignof(T),比我的还简单。
【解决方案2】:

分配一块内存后,我想知道块中每个对象之间需要多少填充来优化 CPU 读/写。

对象之间的填充精确为零;您不允许添加填充。在 C++ 标准库分配器模型中,您的 allocator<T>::allocate(count) 方法需要分配足够的空间来存储 count 类型的 T 对象数组。 C++ 中的数组是紧密封装的;从数组中的一个T 到另一个T 的偏移量必须是sizeof(T)

因此,您不能在分配的存储空间中的对象之间插入填充。您可以在分配的内存块的开头插入填充,这样您就可以准确地使用alignof(T)(您的allocator<T>::allocate 也需要遵守)。但是返回的指针必须是指向Ts 的对齐存储的指针。因此,如果您在分配的前面有填充,则需要在调用 deallocate 时撤消填充,因为它只获取对齐的存储地址。

当涉及到包含基本类型的结构的对齐时,您依赖编译器将其对齐要求强加于这些结构。所以对于这个定义:

struct U
{
  std::int32_t i;
  std::int64_t j;
};

如果编译器认为 int64_t 使用 8 字节对齐会更优化,那么编译器将在 U 中的 ij 之间插入适当的填充。 sizeof(U) 将是 16,alignof(U) 将是 8。

创建对齐不是您的工作,并且您不能编译器这样做。您必须尊重您在allocator<T>::allocate 调用中给出的任何类型的对齐方式。

【讨论】:

  • 我的最终目的是优化空间局部性,这就是为什么我尝试为以下情况创建不同的分配器:std::vector<std::unique_ptr<A>> 其中所有指针都是线性存储但所有*As 都是分散的跨内存,如果分配器模型的要求之一是为T 的数组分配紧凑的内存并且我无法添加填充以允许更快的 CPU 读/写,我该怎么办?
  • 也许我应该做的是创建这些自定义分配器,插入填充以便于 CPU 的读写,但不让它们遵循标准分配器模型并使用 stl 容器?然后我可以使用自定义deleter 函数将内存从它提供给unique_ptr
  • @Jorayen: "为什么我尝试为以下情况创建不同的分配器:std::vector<std::unique_ptr<A>> 所有指针都是线性存储的,但所有 *As 都分散在内存中 " 分配器无法解决这个问题。那是一个容器的域。
  • 那么您什么时候想使用自定义内存分配器,例如 Pool/Linear 分配器?
  • @Jorayen:什么容器的“池/线性分配器”?基本的内存分配器不会改变指向对象的指针向量的分散性质。你可以从一个池中分配那些As,但这不关vector的事,所以这不关vector的分配器的事。
猜你喜欢
  • 2012-07-08
  • 1970-01-01
  • 2012-02-22
  • 2012-05-29
  • 1970-01-01
  • 1970-01-01
  • 2011-04-29
  • 2019-08-11
  • 2018-06-28
相关资源
最近更新 更多