【问题标题】:Why do we use bitwise operators & and ~ instead of mathematical operators % when implementing aligned malloc?为什么我们在实现对齐malloc时使用按位运算符&和~而不是数学运算符%?
【发布时间】:2020-08-15 18:30:25
【问题描述】:

在研究Linux操作系统的内存管理时,看到实现对齐的malloc函数的一般方案如下代码:

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;
}

此解决方案存在一个问题,即由于& ~(alignment - 1),它仅在alignment 是2 的幂时才能正常工作。此外,alignment 必须是 size_t 数据类型,以便它适合指针 p1 的数据类型。 由于这些限制,我想到了另一种解决方案:

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;
    }
    offset = (size_t)(p1) % alignment; // offset is used so that I don't have to declare another variable
    p2 = (void **)((size_t)(p1) + (alignment - offset));
    p2[-1] = p1;
    return p2;
}

此解决方案解决了这两个问题,即 alignment 不必既不是 2 的幂也不是 size_t 数据类型。我的问题是,为什么不使用这种实现对齐 malloc 的方式?它有什么缺点让人们选择使用按位运算符&~ 的解决方案?

非常感谢任何帮助。谢谢。

【问题讨论】:

  • 对于初学者,您的代码不会像原始代码那样在p2[-1] 处为void * 留出空间。在您的代码中,p2[-1] 可能在malloc 返回的块之外,这很糟糕。
  • 另外请注意,允许alignment 是 2 的非幂并不是真正有用,因为想要对齐内存块的通常原因是为了更高效的 CPU 操作(或仅允许)在对齐的数据上,并且这些总是2要求的幂。因此,没有人会希望块与 3 或 5 或 13 的倍数对齐,因此没有理由降低代码的效率来支持这一点。

标签: c linux memory-management malloc


【解决方案1】:

我同意你的观点,经典代码有问题,但不完全是提到的那些:

  • alignment 必须是 2 的幂,这是对 POSIX 标准函数 aligned_alloc 的约束。事实上,alignment 必须是大于等于sizeof(size_t) 的 2 的幂,并且在这个标准下 size 参数应该是 alignment 的倍数。

  • alignment 定义为size_t 类型,但这与指针p1 的数据类型无关。事实上,size_tvoid * 的大小可能与 16 位 MSDOS/Windows 中大型模型架构中的情况不同。

    因此代码p2 = (void **)(((size_t)(p1) + offset) & ~(alignment - 1)); 不严格符合。要解决这个问题,可以使用在<stdint.h> 中定义的uintptr_t,它被指定为与void * 具有相同的大小:

      p2 = (void **)(void *)(((uintptr_t)(p1) + offset) & ~(alignment - 1));
    
  • 发布的代码中还有另一个问题:如果alignment 小于sizeof(void *),则p2 可能会因写入void *p1 而未对齐。需要额外的代码来确保alignment 至少与sizeof(void *) 一样大。在实际系统中,这不是问题,因为malloc() 必须返回针对所有基本类型正确对齐的指针,包括void *

首选按位 &~ 运算符的原因是效率之一:对于 x 一个无符号整数和 alignment 2 的幂 x & (alignment - 1) 等价于 x % alignment,但它很多大多数 CPU 使用按位掩码计算比使用除法更快,并且编译器无法假设 alignment 是 2 的幂,因此它会使用慢得多的整数除法指令编译您的代码。

此外,您的计算不正确:如果p1 未对齐,则offset(计算为(size_t)(p1) % alignment)可以与alignment - 1 一样大,因此p2 可以与p1 一样接近1 byte,所以p2[-1] = p1; 会在分配空间开始之前写入。

这是修改后的版本:

#include <stdint.h>
#include <stdlib.h>

void *aligned_malloc(size_t size, size_t alignment) {
    // alignment must be a power of 2
    //assert(alignment != 0 && (alignment & (alignment - 1)) == 0);
    void *p1;     // allocated block
    void **p2;    // aligned block
    size_t slack; // amount of extra memory to allocate to ensure proper alignment
                  // and space to save the original pointer returned by malloc.
    //compute max(alignment - 1, sizeof(void*) - 1) without testing:
    size_t alignment_mask = (alignment - 1) | (sizeof(void *) - 1);
    slack = alignment_mask + sizeof(void *);
    if ((p1 = malloc(size + slack)) == NULL)
        return NULL;
    p2 = (void **)(void *)(((uintptr_t)p1 + slack) & ~alignment_mask);
    p2[-1] = p1;
    return p2;
}

【讨论】:

    【解决方案2】:

    可能是因为alignment 是一个(对于编译器而言)未知值的变量,这意味着编译器无法将其作为更简单的操作轻松操作。

    按位运算比% 运算符所需的算术(尤其是除法)更简单、更高效。

    【讨论】:

    • alignment 在原始版本中也是一个变量。 (我同意第二段。)
    • 此外,出于性能原因,老派程序员有简单地避免使用/% 的习惯,并在适用时使用相应的移位/掩码编写代码。如果编译器可以自己进行转换,那么如果程序员已经做了同样的事情,它不会损害性能,如果编译不能,那么程序员会产生更快的代码。这个具体的例子是编译器不能(因为它不知道对齐值)的情况之一,并且使用位操作的反射确实得到了回报。
    • 所以我的第二个解决方案是正确的,只是不如使用&amp;~ 运算符那么有效,对吧?因为我不确定它是错误的(在某些情况下或其他一些计算机架构中)还是效率不高。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-23
    • 1970-01-01
    • 2014-07-01
    • 1970-01-01
    • 2012-08-10
    相关资源
    最近更新 更多