对齐 malloc 实现的解释

Posted

技术标签:

【中文标题】对齐 malloc 实现的解释【英文标题】:explanation to aligned malloc implementation 【发布时间】:2016-10-31 12:29:01 【问题描述】:

这不是家庭作业,这纯粹是为了我自己的个人教育。

我不知道如何实现对齐的 malloc,所以在网上查找并找到了this website。为了阅读方便,我将代码贴在下面:

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

void* aligned_malloc(size_t required_bytes, size_t alignment)

    void* p1; // original block
    void** p2; // aligned block
    int offset = alignment - 1 + sizeof(void*);
    if ((p1 = (void*)malloc(required_bytes + offset)) == NULL)
    
       return NULL;
    
    p2 = (void**)(((size_t)(p1) + offset) & ~(alignment - 1));
    p2[-1] = p1;
    return p2;


void aligned_free(void *p)

    free(((void**)p)[-1]);


void main (int argc, char *argv[])

    char **endptr;
    int *p = aligned_malloc (100, strtol(argv[1], endptr, 10));

    printf ("%s: %p\n", argv[1], p);
    aligned_free (p);

实现确实有效,但老实说我无法弄清楚它是如何工作的。

这是我无法理解的:

    为什么我们需要偏移量? anding 与 ~(alignment - 1) 的作用是什么 p2 是一个双指针。为什么我们可以从一个应该只返回一个指针的函数中返回它? 解决此问题的一般方法是什么?

非常感谢任何帮助。

编辑

这不是How to allocate aligned memory only using the standard library? 的重复,因为我还需要知道如何释放对齐的内存。

【问题讨论】:

这仅在 aligned 是 2 的幂并且假定您的对齐至少与 void* 所需的一样大时才有效。 另外:size_t(在设置p2 的行中)应该是uintptr_t。不能保证 size_t 足够大来表示指针值。 How to allocate aligned memory only using the standard library?的可能重复 @Daniel Rudy Proposed duplicate 很好地回答了如何分配对齐的内存。它没有解决也没有回答如何像这段代码尝试那样释放内存。在提议的欺骗中,释放是用原始指针完成的,它的存储没有详细说明。在这里,代码尝试保存/恢复分配块中的原始指针。 @PaulHankin 在您的第一条评论中您说:it assumes your alignment is at least as large as required for void*。我不确定我是否理解这个声明。你能详细说明一下吗? 【参考方案1】:

    如果要支持超出系统malloc() 的对齐方式,则需要偏移量。例如,如果您的系统malloc() 对齐到 8 个字节边界,并且您想要对齐到 16 个字节,则您要求额外增加 15 个字节,这样您就可以确定可以移动结果以按要求对齐它。您还可以将sizeof(void*) 添加到传递给malloc() 的大小,以便为簿记留出空间。

    ~(alignment - 1) 是保证对齐的原因。例如,如果对齐为 16,则减 1 得到 15,即 0xF,然后将其取反得到 0xFF..FF0,这是您需要满足来自malloc() 的任何返回指针的对齐的掩码。请注意,此技巧假定对齐是 2 的幂(实际上通常是这样,但确实应该进行检查)。

    这是一个void**。该函数返回void*。这没关系,因为指向 void 的指针是“指向任何类型的指针”,在这种情况下,该类型是 void*。也就是说,void* 与其他指针类型之间的转换是允许的,双指针仍然是指针。

    这里的总体方案是将原始指针存储在返回给调用者的指针之前。标准malloc() 的一些实现做同样的事情:在返回块之前存储簿记信息。这样可以很容易地知道调用free() 时要回收多少空间。

话虽如此,这种事情通常没有用,因为标准malloc() 返回系统上最大的对齐方式。如果您需要除此之外的对齐方式,可能还有其他解决方案,包括特定于编译器的属性。

【讨论】:

它很有用:将数据与缓存行对齐,以及为奇怪的硬件(例如:一些专业的图形硬件)准备数据是我在现实世界中看到的两种。 您可能会在 2 中注意到 alignment 必须是 2 的幂。就我个人而言,我只使用 % 而不是在这里进行比特旋转 -- malloc 已经相对昂贵并且额外的划分不会对性能产生任何影响。【参考方案2】:

实施确实有效

也许吧,但我不太确定。 IMO 你最好从第一原则开始工作。马上开始,

p1 = (void*)malloc

是一个危险信号。 malloc 返回void。在 C 中,可以从void * 分配任何指针。从malloc 铸造通常被认为是不好的形式,因为它所产生的任何影响都只能是坏的。

为什么我们需要一个偏移量

偏移量为存储malloc 返回的指针提供了空间,稍后由free 使用。

p1 是从 malloc 检索的。稍后,它必须提供给free 才能发布。 aligned_mallocp1 保留sizeof(void*) 字节,将p1 隐藏在那里,并返回p2p1 指向的块中的第一个“对齐”地址)。稍后,当调用者将p2 传递给aligned_free 时,它实际上将p2 转换为void *p2[],并使用-1 作为索引获取原始p1

anding 和 ~(alignment - 1) 完成了什么

这就是将p2 放在边界上的原因。说对齐是16; alignment -1 是 15, 0xF。 ~OxF 是除了最后 4 位之外的所有位。对于任何指针 PP &amp; ~0xF 将是 16 的倍数。

p2 是一个双指针。

指针schmointermalloc 返回void*。这是一块内存;您可以随意解决。你不会眨眼

char **args = calloc(7, sizeof(char*));

分配一个由 7 个 char * 指针组成的数组,你会吗?代码从p1 中选择至少sizeof(void*) 字节的一些“对齐”位置,并且出于free 的目的,将其视为void **

一般做法是什么

没有一个答案。最好的可能是使用标准(或流行)库。如果您在malloc 上构建,分配足够的空间以保留“真实”指针并返回对齐的指针是非常标准的,尽管我会以不同的方式编码。系统调用mmap 返回一个页面对齐指针,它将满足“对齐”的大多数标准。根据需要,这可能比搭载malloc 更好或更差。

【讨论】:

@chux UB的缩写是什么意思? @chux 你介意详细说明为什么 void** 需要对齐。为什么在这种情况下需要对齐?我认为一个具体的例子可能会有所帮助。 @James K. Lowden 我仍然不清楚为什么从应该只返回单个指针的函数返回双指针不会导致错误。您介意对此进行更多详细说明。看起来我遗漏了一些关于 C 的重要内容,但我不太明白它是什么。 @chux 如果它与char ** 对齐,我什至不明白,你能详细说明为什么它需要与char ** 对齐 @chux That value must be aligned for a char *。你能解释一下为什么char * 应该对齐吗?为什么不是int * 或者只是int 或者为什么不是short *,为什么特别是char *【参考方案3】:

假设我们需要 SZ 字节的对齐内存,让:

A is the alignment.
W is the CPU word size.
P is the memory returned by malloc.
SZ is the requested number of bytes to be allocated.

我们将返回 (P + Y) 其中 (P + Y) mod A = 0

所以,我们应该保存原始指针P,以便以后能够释放内存。 在这种情况下,我们应该分配 (SZ + W) 个字节,但是为了让内存对齐,我们将子结构 Z 个字节 其中 (P % A = Z ) => (Z ∈ [0, A-1])

So the total memory to be allocated is:  SZ + W + MAX(Z) = SZ + W + A - 1

要返回的指针是P + Y = P + W + MAX(Z) - (P + W + MAX(Z)) mod A

我们有:X - X mod A = INT(X / A) * A = X & ~(A - 1)

所以我们可以将 P + W + MAX(Z) - (P + W + MAX(Z)) mod A 替换为 (P + W + MAX(Z)) & ~(A - 1)

The memory to be returned is: (P + W + MAX(Z)) & ~(A - 1) = (P + W + A - 1) & ~(A - 1)

【讨论】:

【参考方案4】:

我对这段代码有一些问题。我已将它们编译到以下列表中:

    p1 = (void*)malloc你没有强制转换malloc的返回值。 free(((void**)p)[-1]);你不投免费。 if ((p1 = (void*)malloc(required_bytes + offset)) == NULL) 不要在 if 语句的比较中放置赋值。我知道很多人都这样做,但在我看来,这只是一种糟糕的形式,并且会使代码更难阅读。

他们在这里所做的是将原始指针存储在分配的块中。这意味着只有对齐的指针才会返回给用户。 malloc 返回的实际指针,用户永远不会看到。不过,您必须保留该指针,因为 free 需要它从分配的列表中取消链接块并将其放在空闲列表中。在每个内存块的开头,malloc 将一些内务信息放在那里。诸如 next/prev 指针、大小、分配状态等之类的东西......一些 malloc 的调试版本使用警戒词来检查是否有东西溢出缓冲区。传递给例程的对齐方式必须是 2 的幂。

当我编写自己的 malloc 版本用于池化内存分配器时,我使用的最小块大小为 8 字节。因此,包括 32 位系统的标头在内,总共有 28 个字节(标头为 20 个字节)。在 64 位系统上,它是 40 字节(标题为 32 字节)。当数据与某个地址值(现代计算机系统上的 4 或 8 字节)对齐时,大多数系统的性能都会提高。这样做的原因是因为如果对齐,机器可以在一个总线周期内抓取整个字。如果不是,那么它需要两个总线周期来获取整个单词,然后它必须构造它。这就是编译器在 4 或 8 个字节上对齐变量的原因。这意味着地址总线的最后 2 或 3 位为零。

我知道有一些硬件限制需要比默认的 4 或 8 更多的对齐。如果我没记错的话,Nvidia 的 CUDA 系统需要对齐到 256 字节的东西......这是硬件要求。

这已经被问过了。见:How to allocate aligned memory only using the standard library?

希望这会有所帮助。

【讨论】:

代码使用free(((void**)p)[-1]); 来查找原始指针。如果遵循“不免费”,您将如何编码aligned_free() @chux-ReinstateMonica 我这样做的方法是从指针中减去标头大小,以将其重新指向内务数据块。然后您可以使用普通列表例程将其切换到空闲列表。 代码如何在没有强制转换的情况下“从指针中减去标题大小”?从void * 中减去是UB。

以上是关于对齐 malloc 实现的解释的主要内容,如果未能解决你的问题,请参考以下文章

glibc中malloc的详细解释_转

new,malloc,GlobalAlloc具体解释

malloc和free函数 详细解释

posix_memalign、malloc 和 calloc 与 lli 解释器有问题

使用动态编程实现文本对齐

重新解释适当对齐的指向具有声明类型的对象的指针