malloc如何理解对齐?

71

以下内容摘自此处

pw = (widget *)malloc(sizeof(widget));

调用malloc函数会分配足够大且适当对齐来容纳widget类型对象的原始存储空间。

此外,可以参考herb sutter的fast pImpl文章。他指出:内存对齐。通过new或malloc动态分配的任何内存都保证适合任何类型的对象,但未动态分配的缓冲区不能得到此种保证。

我很好奇,malloc如何知道自定义类型的对齐方式?


6
默认情况下,new和malloc会将地址对齐到8字节(x86)或16字节(x64),这对于大多数复杂数据是最优的。同时,sizeof()函数负责获取正确的结构体大小,包括内部填充以实现对齐。 - Alex Byrth
7个回答

62

对齐要求是递归的: 任何struct的对齐方式仅为其成员中最大的对齐方式,并且这可以递归地理解。

例如,假设每个基本类型的对齐方式等于其大小(通常情况下并非如此),则struct X { int; char; double; }的对齐方式为double,并会填充为double大小的倍数(例如:4 (int), 1 (char), 3 (padding), 8 (double))。 struct Y { int; X; float; }具有X的对齐方式,其最大值与double的对齐方式相等,因此Y的布局将根据其确定:4 (int), 4 (padding), 16 (X), 4 (float), 4 (padding)。

(所有数字仅为示例,可能在您的计算机上不同。)

因此,通过将其分解为基本类型,我们只需要了解少量基本对齐方式,其中有一个众所周知的最大对齐方式。 C++甚至定义了一种类型max_align_t,其对齐方式即为最大对齐方式。

所有malloc()需要做的就是选择一个是该值的倍数的地址。


3
需要指出的关键一点是,这不包括向编译器添加可能会过度对齐数据的自定义“align”指令。 - user541686
4
请注意,即使您使用这些方法,您已经超出了标准范围,但是请注意,以这种方式分配的内存可能无法满足某些平台上可用的扩展类型(例如_m256)的对齐要求。 - jcoder
6
当您使用alignas指定一个自定义对齐方式,其大小大于原始数据类型的最大对齐方式时,会发生什么? - Curious
2
@Curious:支持扩展对齐是由实现定义的。 - Kerrek SB
4
"malloc" 没有关于其分配类型的信息;唯一的参数是所分配内存的大小。手册对此描述得很准确:所分配的内存对齐方式适用于任何数据类型,即所有类型的对齐方式相同。 - Frédéric Dumont
显示剩余6条评论

30
我认为Herb Sutter引用中最相关的部分是我用粗体标记的部分:
对齐。任何内存对齐。通过new或malloc动态分配的任何内存都保证适当地对齐于任何类型的对象,但未动态分配的缓冲区则没有此保证。
它不必知道你心里在想什么类型,因为它是针对任何类型进行对齐的。在任何给定的系统上,都存在一个最大的对齐大小是必要或有意义的;例如,具有四字节字的系统可能具有最多四个字节对齐。
这也可以从malloc(3)手册页中清楚地说明,其中部分内容如下:
malloc()和calloc()函数返回指向已分配内存的指针,该内存适合于任何类型的变量

3
任何类型的变量意味着什么?这并没有回答我的问题。这是否意味着malloc在任何给定的系统中都将使用最大对齐大小,对吗? - Chang
2
@Chang:是的,有效的。另外请注意,引用是错误的。new只有在分配charunsigned char时才保证具有“任何”对齐方式。对于其他类型,它可能具有较小的对齐方式。 - Mooing Duck
1
@aschepler:那不是真的。请参阅C++11规范,第5.3.4节,第10条款;new char[16]以一种被认为保证适合任何类型X的对齐方式指定,其中sizeof(X)<=16 - ruakh
是的。我从同一标准段落中得出了几个非常错误的想法。阅读理解失败。 - aschepler
1
@BenVoigt:我认为“魔法对齐”仅适用于charunsigned char类型,但不适用于signed char类型。C++规范将charunsigned char视为“字节”类型,但不认为signed char是“字节”类型。(暗示地,规范实际上并没有直接说“字节类型”这样的话)。 - Mooing Duck
显示剩余5条评论

5
malloc() 只能使用传递给它的请求的大小信息。通常,它可能会将传递的大小四舍五入到最接近大于(或等于)二次幂的值,并根据该值对内存进行对齐。可能还会有对齐值的上限,例如8个字节。
以上是一种假设性讨论,实际的实现取决于您正在使用的机器架构和运行时库。也许您的malloc()始终返回按8个字节对齐的块,而且它从不执行任何不同的操作。

5
总之,malloc使用“最坏情况”对齐方式是因为它不知道更好的方法。这是否意味着calloc可以更聪明,因为它接受两个参数,对象的数量和单个对象的大小? - Aaron McDaid
1
可能。也可能不是。你需要查看运行库源代码才能找出答案。 - Greg Hewgill
1
-1,抱歉。你的回答包含了真相,但也包含了错误信息。这不是一个“可能,也可能不” 的事情;它被明确记录为以一种不依赖于大小的方式工作。(虽然不知道为什么不这样做。似乎这样做是完全有道理的。) - ruakh
1
我自己的问题的答案是否定的。我找到了这个:"The malloc() and calloc() functions return a pointer to the allocated memory that is suitably aligned for any kind of variable." 不过 memalign 函数似乎也是有用的:http://wwwcgi.rdg.ac.uk:8081/cgi-bin/cgiwrap/wsi14/poplog/man/3C/calloc - Aaron McDaid
看到ruakh的回复,那么malloc在任何给定的系统中都会使用最大对齐大小,是吗? - Chang

3

1) 对齐到所有对齐方式的最小公倍数。例如,如果整数需要4字节对齐,但指针需要8字节对齐,则将所有内容分配到8字节对齐。这会导致所有内容都被对齐。

2) 使用大小参数确定正确的对齐方式。对于小尺寸,您可以推断出类型,例如malloc(1)(假设其他类型大小不为1)始终是一个char。C++的new有类型安全的好处,因此始终可以通过这种方式做出对齐决策。


你能解释一下LCM这个缩写吗?我可以猜测,但我不应该这样做。 - Mooing Duck
此外,在C++中还有其他可以为1字节的类型。但是,您的暗示是正确的,它仍然可以根据类型的大小进行对齐。 - Mooing Duck

2

在C++11之前,对齐方式通常使用未知确切值的最大对齐方式进行处理,并且malloc/calloc仍然以这种方式工作。这意味着malloc为任何类型正确对齐。

根据标准,错误的对齐可能导致未定义行为,但我看到x86编译器很慷慨,只会降低性能。

请注意,您还可以通过编译器选项或指令来微调对齐方式(例如VisualStudio中的pragma pack)。

但是,当涉及到定位new时,C++11带来了新关键字alignofalignas。以下是一些代码,如果编译器的最大对齐方式大于1,则显示其效果。下面的第一个定位new自动完成对齐,但第二个不行。

#include <iostream>
#include <malloc.h>
using namespace std;
int main()
{
        struct A { char c; };
        struct B { int i; char c; };

        unsigned char * buffer = (unsigned char *)malloc(1000000);
        long mp = (long)buffer;

        // First placment new
        long alignofA = alignof(A) - 1;
        cout << "alignment of A: " << std::hex << (alignofA + 1) << endl;
        cout << "placement address before alignment: " << std::hex << mp << endl;
        if (mp&alignofA)
        {
            mp |= alignofA;
            ++mp;
        }
        cout << "placement address after alignment : " << std::hex <<mp << endl;
        A * a = new((unsigned char *)mp)A;
        mp += sizeof(A);

        // Second placment new
        long alignofB = alignof(B) - 1;
        cout << "alignment of B: " <<  std::hex << (alignofB + 1) << endl;
        cout << "placement address before alignment: " << std::hex << mp << endl;
        if (mp&alignofB)
        {
            mp |= alignofB;
            ++mp;
        }
        cout << "placement address after alignment : " << std::hex << mp << endl;
        B * b = new((unsigned char *)mp)B;
        mp += sizeof(B);
}

我想通过一些位运算来提高这段代码的性能。

编辑:用位运算替换了昂贵的取模计算。仍然希望有人能找到更快的方法。


1
实际上,这不是编译器的问题,而是硬件本身的原因。在 x86 架构下,未对齐的内存访问会强制处理器获取内存边界的两侧并将结果拼接在一起,因此总是“正确”的,但速度会变慢。然而,在某些 ARM 处理器上,你会遇到总线错误并导致程序崩溃。这是一个问题,因为很多程序员从未接触过其他架构,可能不知道这种行为实际上是未定义的,而不仅仅是性能下降。 - Thomas
你说得对,它是硬件或CPU微码软件而不是实际的编译器在x86架构上为你节省了。我真的很想知道为什么没有更方便的API来处理这个问题。就好像C/C++设计者想让开发人员陷入陷阱一样。这让我想起了std::numeric_limits<double>::min()陷阱。有人第一次就做对了吗? - Patrick Fromberg
一旦你知道发生了什么,从各种疯狂的类型转换编程风格改为完全类型化的代码并不太难,幸运的是。只要不粗心大意地进行疯狂的位操作,C类型系统就可以相对容易地保持类型对齐。另一方面,没有指针别名的代码具有更加严格的语义... - Thomas
我不理解。每当您拥有自己管理的小堆栈时,就会出现问题。您在评论中考虑使用放置new的用途是什么? - Patrick Fromberg

1

malloc不知道它分配的内容是什么,因为它的参数只是总大小。它只会对齐到任何对象都安全的对齐方式。


1
你可以使用这个小的C程序来查找你的malloc()实现的分配位:
#include <stdlib.h>
#include <stdio.h>

int main()
{
    size_t
        find = 0,
        size;
    for( unsigned i = 1000000; i--; )
        if( size = rand() & 127 )
            find |= (size_t)malloc( size );
    char bits = 0;
    for( ; !(find & 1); find >>= 1, ++bits );
    printf( "%d", (int)bits );
}

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