【问题标题】:What alignment issues limit the use of a block of memory created by malloc?哪些对齐问题限制了 malloc 创建的内存块的使用?
【发布时间】:2014-02-03 12:11:39
【问题描述】:

我正在为 C 中的各种数学计算编写一个库。其中一些需要一些“临时”空间——用于中间计算的内存。所需空间取决于输入的大小,因此不能静态分配。该库通常用于使用相同大小的输入执行相同类型计算的多次迭代,因此我不希望每次调用都在库中使用mallocfree;一次分配足够大的块,将其重新用于所有计算,然后释放它会更有效。

我的预期策略是请求一个指向单个内存块的void 指针,可能附带一个分配函数。说,像这样:

void *allocateScratch(size_t rows, size_t columns);
void doCalculation(size_t rows, size_t columns, double *data, void *scratch);

这个想法是,如果用户打算做几个相同大小的计算,他可以使用 allocate 函数来抓取一个足够大的块,然后使用同一块内存来执行每个计算输入。 allocate 函数不是绝对必要的,但它简化了接口,使将来更容易更改存储需求,而不需要库的每个用户确切知道需要多少空间。

在许多情况下,我需要的内存块只是double 类型的大型数组,没有问题。但在某些情况下,我需要混合数据类型——比如一个双精度块和一个整数块。我的代码需要是可移植的,并且应该符合 ANSI 标准。我知道可以将 void 指针转换为任何其他指针类型,但如果我尝试对两种类型使用相同的块,我会担心对齐问题。

所以,具体的例子。假设我需要 3 个 doubles 和 5 个 ints 块。我可以像这样实现我的功能吗:

void *allocateScratch(...) {
    return malloc(3 * sizeof(double) + 5 * sizeof(int));
}

void doCalculation(..., void *scratch) {
    double *dblArray = scratch;
    int *intArray = ((unsigned char*)scratch) + 3 * sizeof(double);
}

这合法吗?在此示例中对齐可能正常,但如果我切换它并首先采用 int 块和第二个 double 块,这将改变 double 的对齐方式(假设为 64 位双精度和 32 位整数)。有一个更好的方法吗?还是我应该考虑一种更标准的方法?

我最大的目标如下:

  • 如果可能的话,我想使用单个块,这样用户就不必处理多个块或需要不断变化的块数。
  • 我希望该块是malloc 获得的有效块,因此用户可以在完成后调用free。这意味着我不想创建一个小的struct,它具有指向每个块的指针,然后分别分配每个块,这需要一个特殊的销毁函数;如果这是“唯一”的方式,我愿意这样做。
  • 算法和内存要求可能会发生变化,因此我尝试使用 allocate 函数,以便将来的版本可以为可能不同类型的数据获得不同数量的内存,而不会破坏向后兼容性。

也许这个问题在 C 标准中得到了解决,但我一直没能找到它。

【问题讨论】:

  • 第一个例子没问题,第二个例子你必须填充到下一个可被 sizeof(double) 整除的最近地址。
  • 是的,这正是我所期望的。可以保证可以接受吗?似乎尝试手动确保正确对齐可能不值得。
  • 是的,没错。这样你只会浪费最后一个成员的记忆。如果您使用工会,您将浪费每个成员。只要确保您知道 int 结束和 double 开始的位置。
  • (注意:您不能对void* 执行算术运算,因此在您的scratch + N*sizeof(double) 中您应该将scratch 转换为char*,或double*,然后只添加N,最后将其重新转换为int*
  • @ShinTakezou 哦,是的,很好!显然,我没有编译我的示例代码:)。

标签: c memory dynamic-memory-allocation memory-alignment


【解决方案1】:

单个malloc的内存可以划分为多个数组使用,如下图。

假设我们想要 A、B 和 C 类型的数组,其中包含 NA、NB 和 NC 元素。我们这样做:

size_t Offset = 0;

ptrdiff_t OffsetA = Offset;           // Put array at current offset.
Offset += NA * sizeof(A);             // Move offset to end of array.

Offset = RoundUp(Offset, sizeof(B));  // Align sufficiently for type.
ptrdiff_t OffsetB = Offset;           // Put array at current offset.
Offset += NB * sizeof(B);             // Move offset to end of array.

Offset = RoundUp(Offset, sizeof(C));  // Align sufficiently for type.
ptrdiff_t OffsetC = Offset;           // Put array at current offset.
Offset += NC * sizeof(C);             // Move offset to end of array.

unsigned char *Memory = malloc(Offset);  // Allocate memory.

// Set pointers for arrays.
A *pA = Memory + OffsetA;
B *pB = Memory + OffsetB;
C *pC = Memory + OffsetC;

RoundUp 在哪里:

// Return Offset rounded up to a multiple of Size.
size_t RoundUp(size_t Offset, size_t Size)
{
    size_t x = Offset + Size - 1;
    return x - x % Size;
}

这使用了noted by R.. 的事实,即类型的大小必须是该类型的对齐要求的倍数。在 C 2011 中,RoundUp 调用中的sizeof 可以更改为_Alignof,当类型的对齐要求小于其大小时,这可能会节省少量空间。

【讨论】:

    【解决方案2】:

    最新的C11 标准具有max_align_t 类型(以及_Alignas 说明符和_Alignof 运算符和<stdalign.h> 标头)。

    GCC 编译器有一个__BIGGEST_ALIGNMENT__ 宏(给出最大尺寸对齐)。也证明了一些与alignment相关的扩展。

    通常,使用2*sizeof(void*)(作为最大的相关对齐方式)在实践中非常安全(至少在我最近听到的大多数系统上;但可以想象奇怪的处理器和系统不是的情况,也许是DSP-s)。可以肯定的是,请研究您的特定实现的 ABIcalling conventions 的详细信息,例如x86-64 ABIx86 calling conventions...

    并且系统malloc 保证返回一个充分对齐的指针(用于所有目的)。

    在某些系统和目标以及某些处理器上,提供更大的对齐可能会带来性能优势(尤其是在要求编译器进行优化时)。您可能必须(或想要)告诉编译器,例如在 GCC 上使用 variable attributes...

    不要忘记根据Fulton

    没有可移植的软件,只有被移植的软件。

    但是intptr_tmax_align_t 可以为您提供帮助......

    【讨论】:

    • 所以,澄清一下,您的意思是,只要我将暂存空间分割成sizeof(max_align_t) 的倍数的块,就可以了?
    • 仔细阅读问题... 他想要一块内存,其中一些将保存(例如)double 序列,而其中一些将保存(例如)@ 序列987654341@。在这种情况下,您的任何观察都没有帮助。
    • +1 表示 c11 具有 max_align_t。它是用 hack 定义的。
    【解决方案3】:

    请注意,任何类型所需的对齐方式必须均分类型的大小;这是数组类型表示的结果。因此,在没有 C11 特性来确定类型所需的对齐方式时,您可以保守地估计并使用类型的大小。换句话说,如果您想从malloc 中分割出一部分分配用于存储doubles,请确保它从sizeof(double) 的倍数的偏移量开始。

    【讨论】:

    • 这样可以保证安全吗?还是有可能?
    • @BasileStarynkevitch:你怎么认为这很有可能?对象的大小必须是其对齐要求的倍数(由于 C 对数组的要求)。因此,如果从分配开始的偏移量(保证对任何具有基本对齐要求的对象进行适当对齐)是对象大小的倍数,则该地址将针对该对象进行适当对齐。
    • 它与您的特定实现的ABI 规范有关。
    • @BasileStarynkevitch:我不明白这如何回答这个问题。请展示一个示例,其中将对象与其大小的倍数对齐不会产生对象所需的对齐方式。
    • 假设我的主张是错误的。然后,如果您只是将整个 malloc 块用作相关 thpe 的数组,则数组的第 n 个元素(其中 n 是我的声明中提到的类型大小的倍数)将不对齐。这是错误的,所以我的说法是正确的。
    【解决方案4】:

    如果用户正在调用你的库的分配函数,那么他们应该调用你的库的释放函数。这是非常典型(也很好)的界面设计。

    所以我会说只是为您的不同类型使用指向不同池的指针结构。它干净、简单且可移植,任何阅读您的代码的人都会准确地看到您在做什么。

    如果您不介意浪费内存并坚持使用单个块,则可以使用所有类型创建一个联合,然后分配这些类型的数组...

    试图在一个大块中找到适当对齐的内存只是一团糟。我什至不确定您是否可以随身携带。有什么计划?将指针转换为intptr_t,进行一些舍入,然后再转换回指针?

    【讨论】:

    • 我考虑过 union 选项,但似乎有点浪费。最后,我可能会选择“指针结构”解决方案,因为它绝对正确(并且清晰)。我可能工作太努力以避免多次分配。
    • 可能。此外,在 ANSI C 中没有 intptr_t 并且要“手动”对第二个块执行对齐,您需要将其转换为正确的整数类型,从而产生不那么便携的代码。
    • 在这种情况下,我希望结构只包含经常使用的项目,如果不总是使用的话。
    猜你喜欢
    • 2010-09-07
    • 2018-07-22
    • 1970-01-01
    • 1970-01-01
    • 2013-06-27
    • 2015-05-19
    • 2020-06-22
    • 1970-01-01
    • 2011-05-21
    相关资源
    最近更新 更多