自定义堆栈分配器中的 C++ 内存对齐

Posted

技术标签:

【中文标题】自定义堆栈分配器中的 C++ 内存对齐【英文标题】:C++ Memory alignment in custom stack allocator 【发布时间】:2011-08-29 23:01:49 【问题描述】:

通常数据根据其大小以两个地址的幂对齐。

我应该如何对齐大小为 20 字节或其他非二次方大小的结构或类?

我正在创建一个自定义堆栈分配器,所以我猜编译器不会为我对齐数据,因为我正在使用连续的内存块。

更多上下文:

我有一个使用 malloc() 分配大量数据的分配器类。 然后我使用 void* allocate(U32 size_of_object) 方法返回我可以存储是否需要存储对象的指针。 这样,所有对象都存储在同一内存区域中,并且有望适合缓存,从而减少缓存未命中。

【问题讨论】:

"通常数据根据其大小以两个地址的幂对齐。" - 你从哪里得到那个的??在你通常的地址空间中没有那么多的 2 的幂,可能是 20-50...我当然希望我们可以分配更多的对象! 无论如何,您通常不需要担心对齐问题。 你到底想做什么?您的编译器会自动处理许多对齐注意事项。您是否尝试使用 SSE 指令或具有对齐约束的东西? 从什么时候开始,物体的大小是 2 的幂?对于我在这里值得信赖的长替身来说,这甚至都不是真的……而且,这根本不是你在问题中所说的。 “多重”和“权力”这两个词的含义不同。 @Tiago Costa:“二的幂”??你确定他们没有说“二的倍数”,因为这些东西非常不同...... 【参考方案1】:

C++11 有专门用于此目的的 alignof 运算符。不要使用其他帖子中提到的任何技巧,因为它们都有边缘情况,或者可能会因某些编译器优化而失败。 alignof 运算符由编译器实现,并且知道所使用的准确对齐方式。

See this description of c++11's new alignof operator

【讨论】:

alignof 运算符是专门为分配器而设计的。其他提到不注意对齐的人可能会导致速度下降,并且在调用具有强制转换的内存块的函数时可能会导致调用堆栈问题。你有权利关注这一点。【参考方案2】:

尽管编译器(或解释器)通常在对齐的边界上分配单个数据项,但数据结构通常具有具有不同对齐要求的成员。为了保持正确对齐,翻译器通常会插入额外的未命名数据成员,以便每个成员都正确对齐。此外,作为一个整体的数据结构可以用最终的未命名成员填充。这允许结构数组的每个成员正确对齐。 http://en.wikipedia.org/wiki/Data_structure_alignment#Typical_alignment_of_C_structs_on_x86

这表示编译器会在 99.9% 的时间内为您处理好它。至于如何强制一个对象以特定的方式对齐,那是编译器特定的,并且只在某些情况下有效。

MSVC:http://msdn.microsoft.com/en-us/library/83ythb65.aspx

__declspec(align(20)) 
struct S int a, b, c, d; ;
//must be less than or equal to 20 bytes

GCC:http://gcc.gnu.org/onlinedocs/gcc-3.4.0/gcc/Type-Attributes.html

struct S int a, b, c, d;  
__attribute__ ((aligned (20)));

我不知道执行此操作的跨平台方式(包括宏!),但某处可能有简洁的宏。

【讨论】:

问题是我有一个我自己编写的 void* allocate(U32 size_of_objects) 方法,它返回一个指向内存地址的指针。那么编译器会为我对齐对象吗? msdn.microsoft.com/en-us/library/6ewkz86d(v=vs.80).aspx 说The storage space pointed to by the return value is guaranteed to be suitably aligned for storage of any type of object. 所以是的。 另外,如果你使用 malloc,你会想要使用placement new。 parashift.com/c++-faq-lite/dtors.html#faq-11.10 这个答案似乎没有回答这个问题,即为分配器找到必要的对齐方式。您正在对齐结构布局,而不是为其分配的内存。 @ex0du5 经审查,您是正确的。但是,正确答案在我的前两个 cmets :(【参考方案3】:

除非您想直接访问内存,或者在一块内存中压缩最大数据,否则您不必担心对齐问题——编译器会为您处理这种情况。

【讨论】:

【参考方案4】:

由于处理器数据总线的工作方式,您要避免的是“错位”访问。通常,您可以在一次访问中从四的倍数的地址中读取 32 位值;如果你试图从一个不是倍数的地址读取它,CPU 可能不得不将它分成两块或更多块来抓取。因此,如果您真的担心这种细节级别的事情,那么您需要关注的不是整体结构,而是其中的各个部分。您会发现编译器会经常使用虚拟字节填充结构以确保对齐访问,除非您特别强制它们不要使用 pragma。

【讨论】:

【参考方案5】:

既然您现在已经添加了您实际上想要编写自己的分配器,答案很简单:只需确保您的分配器返回一个指针,其值是请求大小的倍数。对象的大小本身已经经过适当调整(通过内部填充),以便所有成员对象本身都正确对齐,因此如果您请求 sizeof(T) 字节,您的分配器需要做的就是返回一个指针,其值可被 @ 整除987654322@.

如果您的对象的大小确实为 20(由 sizeof 报告),那么您无需再担心。 (在 64 位平台上,对象可能会被填充到 24 字节。)

更新:事实上,正如我现在才意识到的那样,严格来说你只需要确保指针是递归对齐的,对于最大的成员你的类型。这可能更有效,但对齐整个类型的大小绝对不会出错。

【讨论】:

【参考方案6】:

我应该如何对齐大小为 20 字节或其他非二次方大小的结构或类?

对齐是特定于 CPU 的,因此如果不知道目标 CPU,就无法回答这个问题。

一般来说,对齐不是您必须担心的事情。您的编译器将为您实施规则。它确实偶尔会出现,例如在编写分配器时。 The C Programming Language (K&R) 中讨论了经典解决方案:使用最差的对齐方式。 malloc 这样做了,尽管 it's phrased as,“如果分配成功,则返回的指针应适当对齐,以便它可以分配给指向任何类型对象的指针。”

这样做的方法是使用unionunion 的元素都分配在union 的基地址上,因此union 必须对齐方式使得每个元素都可以存在于该地址;即union 的对齐方式将与具有最严格规则的元素的对齐方式相同):

typedef Align long;
union header 
    // the inner struct has the important bookeeping info
    struct 
        unsigned size;
        header* next; 
     s;
    // the align member only exists to make sure header_t's are always allocated
    // using the alignment of a long, which is probably the worst alignment
    // for the target architecture ("worst" == "strictest," something that meets
    // the worst alignment will also meet all better alignment requirements)
    Align align;
;

通过创建一个足够大的headers 数组(使用sbrk() 之类的东西)来分配内存以满足请求,再加上一个实际包含簿记信息的额外header 元素。如果数组名为arry,则簿记信息位于arry[0],而返回的指针指向arry[1]next 元素用于遍历空闲列表)。

This works, but can lead to wasted space(“在 Sun 的 HotSpot JVM 中,对象存储与最近的 64 位边界对齐”)。我知道a better approach 试图获得特定于类型的对齐方式,而不是“适用于任何事物的对齐方式”。

编译器通常还具有特定于编译器的命令。它们不是标准的,它们要求您了解相关类型的正确对齐要求。我会避开他们。

【讨论】:

以上是关于自定义堆栈分配器中的 C++ 内存对齐的主要内容,如果未能解决你的问题,请参考以下文章

C++ 类内存模型和对齐

c++内存示例001对齐,坏弱针

c++内存示例004realloc,释放,对齐分配

内存对齐分配策略(含位域模式)

Visual Studio2008 C++结构体成员需要内存对齐吗?

分配初始化的、对齐的内存