为什么C编译器不能重新排列struct成员以消除对齐填充?

Hal*_*ast 89 c struct memory-alignment compiler-optimization

可能重复:
为什么GCC不优化结构?
为什么C++不能使结构更紧凑?

请考虑以下32位x86计算机上的示例:

由于对齐约束,以下结构

struct s1 {
    char a;
    int b;
    char c;
    char d;
    char e;
}
Run Code Online (Sandbox Code Playgroud)

如果成员被重新排序,则可以更有效地表示内存(12对8字节)

struct s2 {
    int b;
    char a;
    char c;
    char d;
    char e;
}
Run Code Online (Sandbox Code Playgroud)

我知道C/C++编译器不允许这样做.我的问题是为什么语言是这样设计的.毕竟,我们最终可能会浪费大量的内存,而且struct_ref->b不会关心差异.

编辑:谢谢大家的非常有用的答案.您解释为什么由于语言的设计方式,重新排列不起作用.然而,它让我想到:如果重新排列是语言的一部分,这些论点是否仍然有效?让我们说有一些指定的重新排列规则,我们至少需要这个规则

  1. 我们应该只在实际需要时重新组织结构(如果结构已经"紧",不要做任何事情)
  2. 规则只查看结构的定义,而不是内部结构.这可确保结构类型具有相同的布局,无论它是否在另一个结构中是内部的
  3. 给定结构的编译内存布局是可预测的,因为它的定义(即规则是固定的)

我一个接一个地论证你的论点:

  • 低级别的数据映射,"最惊喜的元素":只写你的结构以紧身款式自己(像@佩里的回答),并没有发生任何改变(要求1).如果由于一些奇怪的原因,你想要内部填充,你可以使用虚拟变量手动插入,和/或可能有关键字/指令.

  • 编译器差异:要求3消除了这种担忧.实际上,从@David Heffernan的评论来看,我们今天似乎遇到了这个问题,因为不同的编译器填充不同?

  • 优化:重新排序的重点是(内存)优化.我在这看到很多潜力.我们可能无法一起删除填充,但我没有看到重新排序如何以任何方式限制优化.

  • 类型铸造:在我看来,这是最大的问题.不过,应该有办法解决这个问题.由于规则是用语言修复的,编译器能够弄清楚成员的重新排序方式,并做出相应的反应.如上所述,在您想要完全控制的情况下,始终可以防止重新排序.此外,要求2确保类型安全代码永远不会中断.

我认为这样的规则有意义的原因是因为我发现按结构内容而不是按类型对结构成员进行分组更为自然.当我有很多内部结构时,编译器也更容易选择最适合我的顺序.最佳布局甚至可能是我无法以类型安全的方式表达的布局.另一方面,它似乎使语言更复杂,这当然是一个缺点.

请注意,我不是在谈论改变语言 - 只有它可以(/应该)设计不同.

我知道我的问题是假设的,但我认为讨论提供了对机器和语言设计较低层次的更深入的了解.

我在这里很新,所以我不知道是否应该为此产生一个新问题.请告诉我是否是这种情况.

小智 71

C编译器无法自动重新排序字段的原因有多种:

  • C编译器不知道struct代表当前编译单元之外的对象的内存结构(例如:外部库,光盘上的文件,网络数据,CPU页表......).在这种情况下,数据的二进制结构也在编译器无法访问的位置定义,因此重新排序struct字段将创建与其他定义不一致的数据类型.例如,ZIP文件文件标题包含多个未对齐的32位字段.重新排序字段将使C代码无法直接读取或写入标头(假设ZIP实现想直接访问数据):

    struct __attribute__((__packed__)) LocalFileHeader {
        uint32_t signature;
        uint16_t minVersion, flag, method, modTime, modDate;
        uint32_t crc32, compressedSize, uncompressedSize;
        uint16_t nameLength, extraLength;
    };
    
    Run Code Online (Sandbox Code Playgroud)

    packed属性阻止编译器根据字段的自然对齐来对齐字段,并且它与字段排序问题无关.可以对字段进行重新排序,以LocalFileHeader使结构具有最小尺寸并且使所有字段与其自然对齐对齐.但是,编译器无法选择重新排序字段,因为它不知道该结构实际​​上是由ZIP文件规范定义的.

  • C是一种不安全的语言.C编译器不知道是否将通过与编译器看到的类型不同的类型访问数据,例如:

    struct S {
        char a;
        int b;
        char c;
    };
    
    struct S_head {
        char a;
    };
    
    struct S_ext {
        char a;
        int b;
        char c;
        int d;
        char e;
    };
    
    struct S s;
    struct S_head *head = (struct S_head*)&s;
    fn1(head);
    
    struct S_ext ext;
    struct S *sp = (struct S*)&ext;
    fn2(sp);
    
    Run Code Online (Sandbox Code Playgroud)

    这是一种广泛使用的低级编程模式,特别是如果标头包含位于标头之外的数据的类型ID.

  • 如果某个struct类型嵌入了另一种struct类型,则无法内联struct:

    struct S {
        char a;
        int b;
        char c, d, e;
    };
    
    struct T {
        char a;
        struct S s; // Cannot inline S into T, 's' has to be compact in memory
        char b;
    };
    
    Run Code Online (Sandbox Code Playgroud)

    这也意味着将某些字段移动S到单独的结构会禁用某些优化:

    // Cannot fully optimize S
    struct BC { int b; char c; };
    struct S {
        char a;
        struct BC bc;
        char d, e;
    };
    
    Run Code Online (Sandbox Code Playgroud)
  • 由于大多数C编译器都在优化编译器,因此重新排序struct字段需要实现新的优化.值得怀疑的是,这些优化是否能够比程序员能够编写的更好.手动设计数据结构比其他编译器任务(如寄存器分配,函数内联,常量折叠,将switch语句转换为二进制搜索等)耗时得多.因此,通过允许编译器优化数据结构可以获得好处似乎没有传统的编译器优化那么有形.

  • 我接受类型转换的论点。但是我不确定我是否看到了优化的问题。我只是认为编译器可以正确地对待该结构,就好像我自己以有序形式编写了它一样。当然,有些情况下我们无法完全压缩,但无论如何都是这样。另外,关于您的第一个论点:如果标准_需要_特定的重新排序,那么这个论点仍然成立吗?有人可能会说,当您编写一个结构体来表示某些外部数据时,您应该注意重新排序并相应地选择您的类型。 (4认同)

Per*_*rry 30

C的设计和目的是使用高级语言编写非便携式硬件和格式相关代码成为可能.程序员背后的结构内容重新排列会破坏这种能力.

从NetBSD的ip.h中观察这个实际代码:


/*
 * Structure of an internet header, naked of options.
 */
struct ip {
#if BYTE_ORDER == LITTLE_ENDIAN
    unsigned int ip_hl:4,       /* header length */
             ip_v:4;        /* version */
#endif
#if BYTE_ORDER == BIG_ENDIAN
    unsigned int ip_v:4,        /* version */
             ip_hl:4;       /* header length */
#endif
    u_int8_t  ip_tos;       /* type of service */
    u_int16_t ip_len;       /* total length */
    u_int16_t ip_id;        /* identification */
    u_int16_t ip_off;       /* fragment offset field */
    u_int8_t  ip_ttl;       /* time to live */
    u_int8_t  ip_p;         /* protocol */
    u_int16_t ip_sum;       /* checksum */
    struct    in_addr ip_src, ip_dst; /* source and dest address */
} __packed;
Run Code Online (Sandbox Code Playgroud)

该结构在布局上与IP数据报的标题相同.它用于直接解释由以太网控制器作为IP数据报报头的内存blob.想象一下,如果编译器任意地从作者下面重新安排了内容 - 这将是一场灾难.

是的,它不是精确可移植的(甚至还有通过__packed宏给出的非便携式gcc指令),但这不是重点.C专门设计用于编写用于驱动硬件的非便携式高级代码.这是它在生活中的功能.


per*_*eal 11

C [和C++]被认为是系统编程语言,因此它们通过指针提供对硬件的低级访问,例如存储器.程序员可以访问数据块并将其转换为结构并[轻松]访问各种成员.

另一个例子是类似下面的结构,它存储可变大小的数据.

struct {
  uint32_t data_size;
  uint8_t  data[1]; // this has to be the last member
} _vv_a;
Run Code Online (Sandbox Code Playgroud)

  • 忘记文件,结构可以映射到实际的硬件寄存器/地址.编译器不太可能移动芯片来适应. (5认同)

Joh*_*ode 10

不是WG14的成员,我不能说明确,但我有自己的想法:

  1. 它会违反最不惊讶的原则 - 可能有一个很好的理由,为什么我想按特定的顺序排列我的元素,无论它是否是最节省空间的,我不希望编译器重新排列那些元素;

  2. 它有可能打破一些非常重要的现有代码 - 那里有很多遗留代码依赖于结构的地址与第一个成员的地址相同的东西(看到很多经典的MacOS)做出这个假设的代码);

所述C99理由直接解决的第二个点("现有码是重要的,现有的实现不是"),并间接地解决了第一个("信任程序员").


vic*_*tcu 9

它将改变指针操作的语义以重新排序结构成员.如果您关心压缩内存表示,那么作为程序员,您有责任了解目标体系结构,并相应地组织结构.

  • "它将改变指针操作的语义,以重新排序结构成员." 我不知道你的意思.你能详细说明吗?请记住,问题是为什么语言的设计方式是如此,所以一个好的答案必须给出语言设计动机. (5认同)

lar*_*sks 6

如果您正在向C结构读取/写入二进制数据,那么struct成员的重新排序将是一场灾难.例如,没有实际的方法来从缓冲区实际填充结构.

  • 实际上,考虑到C被设计为一种系统语言 - 而这种工作是典型的网络和/或基于文件的i/o - 我很确定这是*正是*那种促使语言设计的东西. (3认同)
  • 但是 C 标准没有说明任何有关打包和对齐的内容,如果您想做您所描述的事情,则需要这样做。换句话说,我无法相信以可移植的方式将结构映射到二进制文件可以成为语言设计的激励因素。 (2认同)
  • 你在这里弄错了.当然,你不能以*portable*方式将结构映射到二进制数据,但C被设计用于生成*非便携式*代码,如嵌入式系统软件,设备驱动程序等,其中非可移植性不是问题,使用高级语言恰恰就是重点. (2认同)

Pet*_*r M 5

结构用于表示最低级别的物理硬件.因此,编译器无法移动一轮以适应该级别.

但是,让#pragma让编译器重新安排纯粹基于内存的结构只是程序内部使用是不合理的.但是我不知道这样的野兽(但这并不意味着蹲下 - 我与C/C++脱节)