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

4
通常情况下,数据的对齐方式与其大小有关,会对齐到二的幂次方地址。
那么对于一个大小为20字节或其他非二的幂次方大小的结构体或类应该如何对齐呢?
我正在创建一个自定义堆栈分配器,因此我想编译器不会为我对齐数据,因为我正在使用连续的内存块进行操作。
一些更多的背景信息:
我有一个分配器类,它使用malloc()来分配大量数据。然后我使用void* allocate(U32 size_of_object)方法来返回指针,以便我可以存储需要存储的对象。这样,所有对象都存储在同一个内存区域中,希望能够适合缓存,减少缓存未命中。

5
通常情况下,数据的对齐地址是二的幂,具体取决于数据的大小。-- 你从哪里得到这个结论的?普通地址空间中并没有那么多二的幂,可能只有20-50种……我真希望我们能够分配更多的对象! - Kerrek SB
2
通常情况下,您不需要担心对齐问题。 - GManNickG
4
你实际上想做什么?你的编译器会自动处理许多对齐方面的考虑。你是要使用SSE指令或具有对齐限制的东西吗? - Darren Engwirda
1
一个对象的大小从何时开始是2的幂次方?即使对于我这里可靠的long double也不是这样...而且,这根本不是你在问题中所说的。"多个"和"幂次方"这两个词的意思是不同的。 - Kerrek SB
1
@Tiago Costa: "2的幂"??你确定他们没有说"2的倍数",因为这两个概念非常不同... - Darren Engwirda
显示剩余8条评论
6个回答

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

1
alignof 运算符是专门为分配器而设计的。其他人提到不注意对齐可能会导致速度潜在下降,并且在调用带有转换内存块的函数时可能会导致调用堆栈问题。你关注这个问题是正确的。 - ex0du5

2
尽管编译器(解释器)通常会在对齐边界上分配单独的数据项,但数据结构经常具有不同对齐要求的成员。为了保持正确的对齐,转换器通常会插入额外的未命名数据成员,以使每个成员正确对齐。此外,整个数据结构可能会用最终未命名的成员填充。这使数组结构的每个成员都可以正确对齐。http://en.wikipedia.org/wiki/Data_structure_alignment#Typical_alignment_of_C_structs_on_x86 这意味着编译器会在大多数情况下自动处理它。至于如何强制对象按特定方式对齐,这取决于编译器,并且仅在某些情况下起作用。
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 GCC是GNU Compiler Collection的缩写,是一款开源编译器。上述链接提供了有关GCC类型属性的详细信息。
struct S{ int a, b, c, d; } 
__attribute__ ((aligned (20)));

我不知道有没有一种跨平台的方式(包括宏!)来做到这一点,但可能有一个很棒的宏存在。


问题是我自己编写了一个 void* allocate(U32 size_of_objects) 方法,它返回一个指向内存地址的指针。那么编译器会为我对齐对象吗? - Tiago Costa
http://msdn.microsoft.com/en-us/library/6ewkz86d(v=vs.80).aspx 表示“返回值指向的存储空间保证适合存储任何类型的对象”,因此是的。 - Mooing Duck
另外,如果您正在使用malloc,您将需要使用放置new。http://www.parashift.com/c++-faq-lite/dtors.html#faq-11.10 - Mooing Duck
2
这个回答似乎没有回答问题,问题是关于找到分配器所需的对齐方式。你正在对齐结构体的布局,而不是为其分配的内存。 - ex0du5
@ex0du5 在审核后,你是正确的。然而,正确答案在我的前两条评论中 :( - Mooing Duck

1

除非你想直接访问内存,或者将最大数据压缩在一块内存中,否则你不需要担心对齐 - 编译器会为你处理。


1
由于处理器数据总线的工作方式,您要避免的是“未对齐”访问。通常,您可以从地址是四的倍数的地方在单次访问中读取32位值;如果您尝试从不是这种倍数的地址读取它,则CPU可能必须分两个或多个部分抓取它。因此,如果您真的担心这个细节级别的事情,您需要关注的不是整体结构,而是其中的部分。您会发现编译器经常使用虚拟字节对结构进行填充,以确保对齐访问,除非您使用#pragma强制禁止它们这样做。

1

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

如果您的对象确实具有大小20(如sizeof所报告的),那么您就没有进一步担心了。(在64位平台上,该对象可能会填充到24个字节。)

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


0
如何对齐大小为20字节或其他非2次幂大小的结构体或类?
对齐是与CPU相关的,因此在不知道目标CPU的情况下,无法回答这个问题。
一般来说,对齐不是你必须担心的事情;编译器已经为你实现了规则。它偶尔会出现,比如在编写分配器时。经典的解决方案在《C程序设计语言》(K&R)中有所讨论:使用最差的对齐方式。malloc就是这样做的,尽管它的措辞是,“如果分配成功,则返回的指针应该适当地对齐,以便可以将其分配给任何类型的对象的指针”。
实现这个的方法是使用一个union(union中的元素都分配在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;
};

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

这样做是可行的,但可能会浪费空间(“在Sun的HotSpot JVM中,对象存储对齐到最近的64位边界”)。我知道一种更好的方法,它尝试获取特定类型的对齐方式,而不是“适用于任何内容的对齐方式”。

编译器通常也有特定于编译器的命令。它们不是标准的,并且需要您了解所涉及类型的正确对齐要求。我建议避免使用它们。


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