C - 可移植地进行类型对齐

C - get type alignment portably

我正在为一种非常简单的语言编写非常小的解释器,它允许简单的结构定义(由其他结构和简单类型组成,如 int、char、float、double 等等)。我希望字段使用尽可能少的对齐方式,所以使用 max_align_t 或类似的东西是不可能的。现在,我想知道是否有更好的方法来对齐除此之外的任何单一类型:

#include <stdio.h>
#include <stddef.h>

#define GA(type, name) struct GA_##name { char c; type d; }; \
    const unsigned int alignment_for_##name = offsetof(struct GA_##name, d);

GA(int, int);
GA(short, short);
GA(char, char);
GA(float, float);
GA(double, double);
GA(char*, char_ptr);
GA(void*, void_ptr);

#define GP(type, name) printf("alignment of "#name" is: %dn", alignment_for_##name);

int main() {
GP(int, int);
GP(short, short);
GP(char, char);
GP(float, float);
GP(double, double);
GP(char*, char_ptr);
GP(void*, void_ptr);
}

这行得通,但也许还有更好的东西?

C11里面有,增加了_Alignof:

printf("Alignment of int: %zu\n", _Alignof(int));

包含 <stdalign.h> 并使用小写 alignof:

通常是更好的样式
#include <stdalign.h>

printf("Alignment of int: %zu\n", alignof(int));

您可以这样检查 C11:

#if __STDC_VERSION__ >= 201112L
    /* C11 */
#else
    /* not C11 */
#endif

如果您使用的是 GCC 或 CLang,则可以通过添加 -std=c11(或者 -std=gnu11 如果您还需要 GNU 扩展)以 C11 模式编译代码。 GCC 的默认模式是 gnu89,CLang 的默认模式是 gnu99


更新:

如果您进行一些有根据的猜测,您可能根本不需要检查系统的对齐情况。我建议使用这两种顺序之一:

// non-embedded use
long double, long long, void (*)(void), void*, double, long, float, int, short, char

// embedded use (microcontrollers)
long double, long long, double, long, float, void (*)(void), void*, int, short, char

这种排序是完全可移植的(但并不总是最优的),因为最坏的情况就是你得到比其他情况更多的填充。

接下来是一个(诚然冗长的)基本原理。如果您不关心我是如何得出这个排序结论的,请随意跳过这一点之后的所有内容。


涵盖大多数情况

这在 C 中成立(无论实现如何):

// for both `signed` and `unsigned`
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
sizeof(float) <= sizeof(double) <= sizeof(long double)

稍微调整一下顺序,您应该能够在 大多数 情况下获得未填充的结构。 请注意,这保证结构将被取消填充;但它会出现在大多数真实世界情况下,这应该是 GoodEnough™。

以下是特定于实现的建议,但应涵盖大多数情况下的对齐。

然而,这在任何地方都是完美可移植的(即使不是最佳的)——只要您接受可能有一些填充(换句话说,不要假设结构没有任何填充)。如果你弄错了,你得到的只是一个更大的结构,所以没有遇到任何类型的未定义行为的危险。

您应该做的是将它们从大到小排列,因为它们的对齐方式也将按此顺序排列。假设一个典型的 amd64 编译器:

long long a; // 8-byte
long b;      // 8-byte or 4-byte; already aligned in both cases
int c;       // 4-byte; already aligned
short d, e;  // 2-byte; both already aligned
char f;      // 1-byte; always aligned

整数类型

所以让我们开始计算顺序,从整数类型开始:

long long, long, int, short, char

浮点类型

现在,浮点类型。你用 double 做什么?它的对齐方式在 64 位架构上 通常 8 字节,在 32 位架构上为 4 字节(但在某些情况下可以是 8 字节)。

long long 总是至少 8 字节(这是标准隐式要求的,因为它的最小范围),并且 long 总是至少 4 字节(但它是 通常 64位中的8字节;也有例外如Windows).

我要做的是将 double 放在它们之间。请注意,double 的大小可以是 4 个字节(通常在嵌入式系统中,例如 AVR / Arduino),但实际上它们总是有一个 4 字节的 long.

long double 是一个复杂的案例。它的对齐范围可以从 4 字节(比如 x86 Linux)到 16 字节(amd64 Linux)。然而,4 字节对齐是一个历史产物并且不是最优的;所以我假设它至少是 8 字节并将其放在 long long 之上。当它的对齐方式是 16 字节时,这也会使其成为最佳。

这剩下 float,它实际上总是一个 4 字节的数量,具有 4 字节对齐;我将把它放在 longint 之间,前者保证至少为 4 字节,后者(通常)可以是 4 或 2 字节。

所有这些结合起来给我们下一个订单:

long double, long long, double, long, float, int, short, char

指针类型

我们现在只剩下指针类型了。不同 非函数 指针的大小不一定相同,但我假设它是(即使不是全部,但在绝大多数情况下都是如此)。我假设函数指针 可以 更大(想想 ROM 比 RAM 更大的硬件架构),所以我会把它们放在其他指针之上。

最坏的实际情况是它们是一样的,所以我一无所获;最好的情况是我消除了更多的填充。

但是大小呢?这通常适用于 非嵌入式 系统:

sizeof(long) <= sizeof(T*) <= sizeof(long long)

在大多数系统中,sizeof(long)sizeof(T*)是一样的;但是例如64 位 Windows 有 32 位 long 和 64 位 T*。但是,在嵌入式系统中,情况就不同了;指针可以是16位的,也就是说:

sizeof(int) <= sizeof(T*) <= sizeof(long)

在这里做什么由您决定 --- 知道通常 运行 在哪里。一方面,针对非主要用途的嵌入式进行优化意味着针对不常见的情况进行优化。另一方面,内存在嵌入式系统中比在非嵌入式系统中更受限制。就个人而言,我建议针对桌面使用进行优化,除非您专门制作嵌入式应用程序。由于 double 的对齐方式通常与指针大小 相同,但可以更大 ,因此我将其放在 double.

下面
// non-embedded
long double, long long, void (*)(void), void*, double, long, float, int, short, char

对于嵌入式应用,我会把它放在 float 下面,因为 float 的对齐通常是 4 字节,而 T* 是 2 字节或 4 字节:

// embedded
long double, long long, double, long, float, void (*)(void), void*, int, short, char

这可能不是很便携,但是 GCC 接受以下内容:

#define alignof(type) offsetof(struct { char c; type d; }, d)

编辑: 根据 this answer,C 允许转换为匿名结构类型(尽管我希望看到此语句得到支持)。所以以下应该是可移植的:

#define alignof(type) ((size_t)&((struct { char c; type d; } *)0)->d)

另一种使用GNU statement expressions的方法:

#define alignof(type) ({ \
    struct s { char c; type d; }; \
    offsetof(struct s, d); \
})