C语言 在内存布局中使用确定性字段排序可以获得什么?



结构体的成员按其在声明中出现的顺序在结构体内部分配,并且地址升序。

我面临着如下的困境:当我需要声明一个结构时,我是否

(1)逻辑分组字段,或者

(2)按大小递减顺序,以节省RAM和ROM的大小?

下面是一个例子,其中最大的数据成员应该在顶部,但也应该与逻辑连接的colour分组:

struct pixel{
    int posX;
    int posY;
    tLargeType ColourSpaceSecretFormula;
    char colourRGB[3];
}

结构的填充是不确定的(也就是说,是依赖于实现的),所以我们不能可靠地对结构元素进行指针运算(而且我们不应该:想象一下有人按照自己的喜好重新排序字段:砰,整个代码停止工作)。

-fpack-structs在gcc中解决了这个问题,但有其他限制,所以我们不考虑编译器选项。

另一方面,代码首先应该是可读的。无论如何都要避免微优化。

那么,我想知道,为什么结构体的成员是按标准排序的,这让我担心以特定的方式对结构体成员排序的微优化?

编译器受到几个传统的和实际的限制。

强制类型转换后指向结构体的指针(标准称之为"适当转换")将等于指向结构体第一个元素的指针。这通常用于在消息传递中实现消息重载。在这种情况下,结构体的第一个元素描述了结构体其余部分的类型和大小。

最后一个元素可以是一个动态调整大小的数组。即使在官方语言支持之前,这种做法也经常在实践中使用。您分配sizeof(struct) + length of extra data,并且可以像访问普通数组一样访问最后一个元素,其中包含您分配的元素数量。

这两件事强制编译器使结构体中的第一个和最后一个元素的声明顺序与声明顺序相同。

另一个实际要求是每次编译都必须以相同的方式对结构体成员排序。一个聪明的编译器可以做出这样的决定:因为它看到一些结构成员总是彼此靠近访问,所以可以以一种使它们最终出现在缓存行的方式对它们进行重新排序。这种优化在C语言中当然是不可能的,因为结构体经常在不同的编译单元之间定义一个API,我们不能在不同的编译中重新排序。

考虑到这些限制,我们所能做的最好的事情是在ABI中定义某种打包顺序,以最大限度地减少不涉及结构体中的第一个或最后一个元素的对齐浪费,但这会很复杂,容易出错,而且可能不会带来太多好处。

如果你不能依赖于排序,那么编写低级代码将结构映射到硬件寄存器,网络数据包,外部文件格式,像素缓冲区等,将会困难得多。

还有,一些代码使用了一种技巧,它假设结构体的最后一个成员是内存中最高寻址的,以表示更大的数据块的开始(在编译时大小未知)。

重新排序结构的字段有时可以在数据大小和代码大小方面获得良好的收益,特别是在64位内存模型中。这里有一个例子来说明(假设通用对齐规则):

struct list {
   int  len;
   char *string;
   bool isUtf;
};

在32位模式下占用12字节,而在64位模式下占用24字节。

struct list {
   char *string;
   int  len;
   bool isUtf;
};

在32位模式下占用12个字节,而在64位模式下只占用16个字节。

如果你有一个这些结构的数组,你将获得50%的数据,但也会增加代码大小,因为索引2的幂比其他大小更简单。如果您的结构是单例的或不频繁的,那么重新排序字段就没有多大意义。如果它被大量使用,它是一个值得关注的点。

关于你问题的另一点。为什么编译器不做字段的重新排序,这是因为在这种情况下,很难实现使用公共模式的结构的联合。例如:
struct header {
    enum type;
    int  len;
};
struct a {
    enum type;
    int  len;
    bool whatever1;
};
struct b {
    enum type;
    int  len;
    long whatever2;
    long whatever4;
};
struct c {
    enum type;
    int  len;
    float fl;
};

 union u {
    struct h header;
    struct a a;
    struct b b;
    struct c c;
 };

如果编译器重新排列字段,这种结构将更加不方便,因为当通过union中包含的不同结构访问typelen字段时,不能保证它们是相同的。如果我没记错的话,标准甚至要求这种行为

最新更新