是否可以在不违反严格别名的情况下将字符数组用作内存池?

Is it possible to use a character array as a memory pool without violating strict aliasing?

我有一个静态分配的字符数组。我可以在不违反严格的别名规则的情况下重用这个数组来存储不同的类型吗?我不太了解严格的别名,但这里有一个代码示例可以完成我想做的事情:

#include <stdio.h>

static char memory_pool[256 * 1024];

struct m1
{
    int f1;
    int f2;
};

struct m2
{
    long f1;
    long f2;
};

struct m3
{
    float f1;
    float f2;
    float f3;
};

int main()
{
    void *at;
    struct m1 *m1;
    struct m2 *m2;
    struct m3 *m3;

    at = &memory_pool[0];
    
    m1 = (struct m1 *)at;
    m1->f1 = 10;
    m1->f2 = 20;

    printf("m1->f1 = %d, m1->f2 = %d;\n", m1->f1, m1->f2);

    m2 = (struct m2 *)at;
    m2->f1 = 30L;
    m2->f2 = 40L;

    printf("m2->f1 = %ld, m2->f2 = %ld;\n", m2->f1, m2->f2);

    m3 = (struct m3 *)at;
    m3->f1 = 5.0;
    m3->f2 = 6.0;
    m3->f3 = 7.0;

    printf("m3->f1 = %f, m3->f2 = %f, m3->f3 = %f;\n", m3->f1, m3->f2, m3->f3);

    return 0;
}

我使用 gcc 和 -Wstrict-aliasing=3 -fstrict-aliasing 编译了这段代码,它按预期工作:

m1->f1 = 10, m1->f2 = 20;
m2->f1 = 30, m2->f2 = 40;
m3->f1 = 5.000000, m3->f2 = 6.000000, m3->f3 = 7.000000;

该代码安全吗?假设 memory_pool 总是足够大。

Is it possible to use a character array as a memory pool without violating strict aliasing?

没有。 C 2018 6.5 7 中的规则说定义为 char 数组的 object 可以访问为:

  1. char
  2. 数组兼容的类型
  3. char
  4. 数组兼容的类型的合格版本
  5. char,
  6. 数组对应的有符号或无符号类型
  7. char,
  8. 数组对应的有符号或无符号类型
  9. 在其成员中包含 char 数组的聚合或联合类型,或
  10. 一种字符类型。

3 和 4 对于 char 的数组是不可能的;它们仅在原始类型为整数类型时适用。在您使用结构的各种示例中,结构的类型与 char 数组不兼容(它们的成员也不兼容),排除了 1 和 2。它们的成员中不包含 char 数组,裁定out 5. 它们不是字符类型,排除 6.

I've compiled this code using gcc with -Wstrict-aliasing=3 -fstrict-aliasing, and it works as intended:

示例输出显示代码在一次测试中产生了所需的输出。这并不等同于显示它按预期工作。

Is that code safe?

没有。在某些情况下可以使代码安全。首先,以适当的对齐方式声明它,例如 static _Alignas(max_align_t) memory_pool[256 * 1024];。 (max_align_t<stddef.h> 中定义。)这使得指针转换部分定义。

其次,如果您使用的是GCC或Clang并请求-fno-strict-aliasing,编译器提供了C语言的扩展,放宽了C 2018 6.5 7。或者,在某些情况下,可以推断出根据编译器和链接器设计的知识,即使违反了 6.5 7,你的程序也能正常工作:如果程序是在单独的翻译单元中编译的,并且 object 模块不包含类型信息或没有花哨的 link-time使用优化,并且在实现内存池的翻译单元中没有发生别名违规,那么违反 6.5 7 就不会产生不利后果,因为 C 实现无法区分在内存池方面违反 6.5 7 的代码来自没有的代码。此外,您必须知道指针转换按预期工作,它们有效地产生指向相同地址的指针(而不仅仅是可以转换回原始指针值但不能直接用作指向同一内存的指针的中间数据) .

没有不良后果的推论是脆弱的,应谨慎使用。例如,在实现内存池的翻译单元中很容易意外违反 6.5 7,如将指针存储在释放的内存块中或将大小信息存储在已分配块之前的隐藏 header 中。

该标准有意避免要求所有实现都适用于低级编程,但允许用于低级编程的实现扩展语言以支持这种用法,方法是在更多情况下指定它们的行为,而不是强制要求标准。然而,即使使用专为低级编程设计的编译器,使用字符数组作为内存池通常也不是一个好主意。然而,为了与最广泛的编译器和平台兼容,应该将内存池对象声明为具有最宽对齐类型的数组,或包含具有最宽对齐类型的 long 字符数组的联合,例如

 static uint64_t my_memory_pool_allocation[(MY_MEMORY_POOL_SIZE+7)/8];
 void *my_memory_pool_start = my_memory_pool_allocation;

 union
 {
   unsigned char bytes[MY_MEMORY_POOL_SIZE];
   double alignment_force;
 } my_memory_pool_allocation;
 void *my_memory_pool_start = my_memory_pool_allocation.bytes;

请注意,clang 和 gcc 可以配置为使用 -fno-strict-aliasing 标志以适合低级编程的方式扩展语言,商业编译器通常甚至可以支持内存池等低级概念使用基于类型的别名时,因为他们将指针类型转换视为可能错误的基于类型的别名假设的障碍。

如果 void* 被初始化为一个静态对象的地址,该对象的符号没有在其他上下文中使用,我认为任何普通的编译器都不会关心用于初始化。在这里跳过障碍以遵循标准是傻瓜的差事。当不使用 -fno-strict-aliasing 时,clang 和 gcc 都不会处理标准规定的所有极端情况,而使用 -fno-strict-aliasing 时,它们将扩展语言的语义以允许使用内存池无论标准是否要求它们都很方便。