对齐内存管理?

21

我有几个关于管理对齐内存块的问题,希望能得到跨平台的答案。但是,由于我相信跨平台的解决方案并不存在,所以我主要关心Windows和Linux,Mac OS和FreeBSD则次之。

  1. 如何最好地获得一个在16字节边界上对齐的内存块?(我知道使用malloc()的简单方法,分配一些额外的空间,然后将指针移动到正确对齐的值。不过,我希望有更少巧妙的方法。另外,下面还有其他问题。)

  2. 如果我使用普通的malloc(),分配额外的空间,然后将指针移动到正确对齐的位置,是否有必要保留块开头的指针以便释放?(在Windows上,在块中间调用free()似乎可以正常工作,但我想知道标准说什么,即使标准说你不能这样做,它是否在所有主要操作系统上都可以正常工作。我不关心像DS9K这样的鲜为人知的操作系统。)

  3. 这是困难/有趣的部分。在保留对齐的情况下,重新分配内存块的最佳方法是什么?理想情况下,这将比调用malloc()、复制,然后在旧块上调用free()更智能。我希望能够就地执行。


关于第三点,如果您正确使用realloc,它几乎总是会调用mallocmemcpy,所以不必担心尝试找到解决方案。 - R.. GitHub STOP HELPING ICE
1
@R,如果realloc不首先尝试将当前块扩展到空闲堆中,那么它将非常糟糕。只有在这种情况不可能时,它才会执行低效的malloc/copy操作。 - paxdiablo
2
在Windows上,在指向块中间的指针上调用free()函数似乎在实践中可行--我对此表示怀疑。 - Jim Balter
3
@Jim,也许由此引起的崩溃只是淹没在Windows普遍崩溃的噪音中而已 :-) [[pax 在众多受冒犯的Windows用户的攻击下躲避]]. - paxdiablo
@Jim:这仅基于我编写的一个非常快速的测试程序。我发现很惊人的是,我已经学会了足够关于内存管理的知识,可以提出这样的问题,而之前从未遇到过调用free()函数释放指向块中间的指针的问题。 - dsimcha
显示剩余2条评论
7个回答

20
  1. 如果你的实现需要16字节对齐的标准数据类型(例如long long),malloc已经保证你返回的块将正确对齐。C99第7.20.3节规定:如果成功分配,则返回的指针适当地对齐,以便它可以赋值给任何类型的对象的指针。

  2. 你必须将与malloc给出的地址完全相同的地址传回到free中。没有例外。所以是的,你需要保留原始副本。

  3. 如果你已经有一个需要16字节对齐的类型,请参见上面的(1)。

除此之外,你可能会发现你的malloc实现为了效率而给你提供了16字节对齐的地址,尽管这不被标准保证。如果你需要,请始终实现自己的分配器。

就我个人而言,我会在malloc之上实现一个malloc16层,该层使用以下结构:

some padding for alignment (0-15 bytes)
size of padding (1 byte)
16-byte-aligned area

你可以编写 malloc16() 函数,调用 malloc 来获取一个比请求的内存块大 16 字节的块,找出对齐区域应该在哪里,将填充长度放在其之前,然后返回对齐区域的地址。

对于 free16,你只需要查看给定地址之前的字节以获取填充长度,从而计算出 malloc 返回的实际块的地址,并将其传递给 free

这个代码没有经过测试,但应该是一个不错的开始:

void *malloc16 (size_t s) {
    unsigned char *p;
    unsigned char *porig = malloc (s + 0x10);   // allocate extra
    if (porig == NULL) return NULL;             // catch out of memory
    p = (porig + 16) & (~0xf);                  // insert padding
    *(p-1) = p - porig;                         // store padding size
    return p;
}

void free16(void *p) {
    unsigned char *porig = p;                   // work out original
    porig = porig - *(porig-1);                 // by subtracting padding
    free (porig);                               // then free that
}

malloc16 中的神奇语句是 p = (porig + 16) & (~0xf);,它会在地址上加上 16,并将低 4 位设置为 0,实际上将其带回到下一个最低对齐点(+16 确保它超过了实际分配块的起始点)。

现在,我不认为上面的代码有任何优秀之处。您必须在相关平台上测试它是否可用。它的主要优点在于抽象了难看的部分,因此您永远不必担心它。


2
来自Linux上的posix_memalign手册:“GNU libc malloc()总是返回8字节对齐的内存地址”。关于7.20.3 - 任何指针的对齐并不意味着它必须是16字节。 - Tony Delroy
2
@Tony,如果你有一个需要16字节对齐的16字节对象,malloc必须返回满足该要求的地址。这不是为指针对齐,而是为了对可指向的对象进行对齐。 - paxdiablo
2
@paxdiablo:C语言要求malloc返回一个适当对齐的指针,但这仅适用于存在于C语言范围内的类型。例如,如果OP使用SSE指令编写汇编代码,则可能需要更大的对齐方式,当然,C实现不负责提供它。此外,OP可能希望指针适合(例如)28位,以便将其他数据紧密地打包到指针中。 :-) - R.. GitHub STOP HELPING ICE
@paxdiablo:这个要求不适用于像__m128这样的类型,因为使用这种类型是正式未定义行为,这使得malloc()免除了所有责任。 - caf
1
如果类型需要16字节对齐而不是UB类型(例如完全有效的128位长整型),那么使用__m128会引入UB。那就是我试图传达的观点:看起来很糟糕 :-) ,但malloc必须遵守对齐要求。 - paxdiablo
显示剩余11条评论

1

在C11中,您可以使用void *aligned_alloc( size_t alignment, size_t size );原语开始,其中参数为:

alignment - 指定对齐方式。必须是实现支持的有效对齐方式。 size - 分配的字节数。对齐的整数倍。

返回值

成功时,返回指向新分配内存开头的指针。返回的指针必须用free()或realloc()释放。

失败时,返回空指针。

示例:

#include <stdio.h>
#include <stdlib.h>


    int main(void)
    {
        int *p1 = malloc(10*sizeof *p1);
        printf("default-aligned addr:   %p\n", (void*)p1);
        free(p1);

        int *p2 = aligned_alloc(1024, 1024*sizeof *p2);
        printf("1024-byte aligned addr: %p\n", (void*)p2);
        free(p2);
    }

可能的输出:
default-aligned addr:   0x1e40c20
1024-byte aligned addr: 0x1e41000

1
  1. 我不知道有任何方法可以请求malloc返回比通常更严格对齐的内存。至于Linux上的“通常”,从man posix_memalign(如果您喜欢,可以使用它来获取更严格对齐的内存):

    GNU libc malloc()始终返回8字节对齐的内存地址,因此仅在需要较大对齐值时才需要这些例程。

  2. 您必须使用由malloc(),posix_memalign()或realloc()返回的相同指针释放内存。

  3. 像往常一样使用realloc(),包括足够的额外空间,以便如果返回的新地址尚未对齐,则可以稍微移动它以对齐它。很糟糕,但这是我能想到的最好的方法。


1
使用memmove来重新对齐比起直接分配新内存更糟糕。在大多数实际情况下,你会触发两个复制操作。 - R.. GitHub STOP HELPING ICE
R:一个很好的警告 - 值得生成一些特定于系统的统计数据,以查看realloc在原地发生的频率,以便做出选择。 - Tony Delroy

1
你可以编写自己的板块分配器来处理对象,它可以使用mmap一次分配一页,维护最近释放地址的缓存以进行快速分配,为您处理所有对齐,并使您能够根据需要精确移动/增长对象。 malloc非常适用于通用分配,但如果您了解数据布局和分配需求,则可以设计一个系统以完全满足这些要求。

1

最棘手的要求显然是第三个,因为任何基于malloc() / realloc()的解决方案都会受到realloc()将块移动到不同对齐方式的影响。

在Linux上,您可以使用使用mmap()创建的匿名映射代替malloc()。由mmap()返回的地址必须与页面对齐,并且可以使用mremap()扩展映射。


0
  1. 在你的系统上进行实验。 在许多系统上(特别是64位系统),您从 malloc()获得16字节对齐的内存。 如果没有,则必须分配额外的空间并移动指针(在几乎所有机器上最多移动8个字节)。

    例如,x86 / 64上的64位Linux具有16字节的 long double ,它是16字节对齐的 - 因此所有内存分配都是16字节对齐的。 但是,在32位程序中, sizeof(long double)为8,内存分配仅为8字节对齐。

  2. 是的 - 您只能 free() malloc()返回的指针。 其他任何操作都可能导致灾难。

  3. 如果您的系统执行16字节对齐的分配,则不存在问题。 如果没有,则您需要自己的reallocator,它执行16字节对齐的分配,然后复制数据 - 或者使用系统 realloc()并在必要时调整重新对齐的数据。

请仔细查看您的malloc()手册页面;可能有选项和机制可以调整它的行为,使其符合您的要求。

在MacOS X上,有posix_memalign()valloc()(提供页面对齐分配),还有一整套“分区malloc”函数,由man malloc_zoned_malloc标识,头文件是<malloc/malloc.h>


1
在你的系统上进行实验。有趣的事实是,很多年前我在Windows上遇到过这个问题。如果你的分配器实际上是一个子分配器,那么在简单的实验中它可能只返回16字节对齐的值。它保持16字节对齐,并且进程中从Windows返回的第一个分配总是16字节对齐的,原因与虚拟内存有关。第二个分配可能不是,所以一旦你用完了子分配器的第一个块,你就有50%的机会获得不太对齐的分配。在我的职业生涯的第一年中,花了将近一周的时间来调试别人的代码。 - Steve Jessop
1
......“某人”已经离开了公司,而问题代码正在将指针向下移动几个位,以释放顶部的空间用于标志(这并不像听起来那么邪恶,因为整个系统出于必要性而被荒谬地优化了内存使用,并且问题代码是一个大数库)。但是,该代码没有包含任何注释,这就像听起来那样邪恶。 - Steve Jessop

-1

你也许可以在Microsoft VC++和其他编译器中使用以下代码:

#pragma pack(16)

这样就可以强制malloc()返回一个16字节对齐的指针。类似于:

ptr_16byte = malloc( 10 * sizeof( my_16byte_aligned_struct ));

如果malloc()能够正常工作,我认为realloc()也同样适用。

这只是一个想法。

-- pete


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