【问题标题】:Why do compilers pad structs in C/C++?为什么编译器在 C/C++ 中填充结构?
【发布时间】:2020-11-27 08:25:06
【问题描述】:

我正在学习结构填充,并读到结构填充背后的原因是,如果结构的成员未对齐,处理器将无法在一个周期内读取/写入它们。通常,由N 字节组成的数据类型的位置应该是N 的倍数的地址。

假设这个结构例如:

struct X
{
    char c;
    // 3 bytes padding here so that i is aligned.
    int i;
};

这里这个struct的大小应该是8个字节,c默认是对齐的,因为它只占1个字节,而i不是。对于i,我们需要在它之前添加 3 个字节的填充,以便它“对齐”并且只能在一个周期内访问。如果我遗漏了什么,请告诉我。

1 - 对齐如何工作?成员与什么结盟?

2 - CPU 访问位于N 倍数地址的N 字节数据类型有什么好处?为什么例如在上面的结构中,如果i位于地址XXX3(以3结尾,换句话说,不是4的倍数),为什么不读取从地址XXX3开始的单词呢?为什么一定是4的倍数?大多数 CPU 访问的地址是否仅为字长的倍数?我相信 CPU 可以从任何字节开始从内存中读取一个单词。我错了吗?

3 - 为什么编译器不对成员重新排序以尽可能多地占用空间?排序重要吗?我不确定是否有人使用实际的偏移量来访问成员。这意味着如果有一个结构X x,通常会像这样访问成员:x.i 而不是*(&x + 4)。在后一种情况下,排序实际上很重要,但在第一种情况下(我相信每个人都使用),排序应该无关紧要。我必须注意,在这个例子中,如果i 出现在c 之前也没关系,最后会有一个 3 字节的填充。我一般问为什么?

4 - 我读到这不再重要了,CPU 现在通常可以访问非对齐成员,花费与对齐成员相同的时间。真的吗?如果是,那为什么?

最后,如果有好的地方可以了解更多,我将不胜感激。

【问题讨论】:

  • 对齐是一个平台架构约束。与某些架构上的对齐访问一样,未对齐的数据访问可能代价高昂(性能高达 x16),并且可能阻碍原子读/写(仅与多线程应用程序相关),或者完全不受支持(导致进程故障)。其他架构可以毫无问题地处理它们,其他架构也可以处理它们,但会降低性能(因此编译器会在性能方面出错)。
  • @ssd 如问题 2 所述,如果我在地址 XXX3 处有一个 double,那么为什么不从地址 XXX3 开始读取整个 8 个字节? CPU不能访问内存中的任何位置吗?为什么这个例子中的地址应该是 8 的倍数?
  • @StackExchange123 :是的,CPU 可以访问那块内存,您可以通过编写自己的汇编代码来实现这一点。编译器只是优化成块读取。
  • @StackExchange123 :我用谷歌搜索发现某些 CPU(例如 arm)具有编译器指令(-munaligned-access),您可以关闭此对齐访问的东西.
  • 阅读此article

标签: c++ c struct padding memory-alignment


【解决方案1】:

C 和 C++ 不是兼容的标记。选择一个。

处理器访问自然对齐的对象比访问未对齐的对象需要更少的逻辑。

这看起来像是 1970 年代的响应,但稍微快进一点,想象一下从地址 0x1ffffff 加载一个 4 字节的数量。

cpu 到底是做什么的?向内存系统询问 0x1ffffff 处的字节,然后是 0x2000000 处的 long,然后将它们移位并屏蔽到寄存器中?

这听起来还不错,直到您意识到它需要两个单独的内存事务来实现这一点。那很不好。另一个 CPU 可能在干预操作中重写了其中的一部分,所以我们的负载是无效的。

将总线锁定协议扩展为处理多个事务可能是一个不可行的方法:要让总线协议按原样工作需要做很多工作。

在实践中,现代系统使用缓存对齐访问,因此如果您的未对齐访问驻留在缓存行中,可能没问题,但一旦不是,您就会受到未指定总线控制器等的摆布。 .

【讨论】:

  • 我认为对于填充,C 和 C++ 编译器的行为方式相同。我错了吗?
  • @StackExchange123 它是实现定义的,因此不是由标准控制,而是由平台的二进制格式规范控制。然而,C 和 C++ 标准涵盖了关于它如何发生或如何控制它的定义。它们的行为方式可能相同,更多的标准建议它们为 POD 结构和类似对象提供兼容的格式,但仅此而已。这是两种不同的语言,有不同的规则。
【解决方案2】:

1 对齐是如何工作的?

对象的内存分配在满足类型对齐要求的内存位置。即:对于N的对齐要求,内存位置的地址将被N整除。

1 成员与什么结盟?

对象与目标系统上该对象类型的对齐方式对齐。这对所有对象都是一样的,包括成员对象。

2 大多数 CPU 访问的地址是否仅为字长的倍数?

一些 CPU 确实只访问对齐的地址。

2 我相信 CPU 可以从内存中从任何字节开始读取一个字。我错了吗?

在某些 CPU 的情况下,您并没有错。如果您认为这适用于所有 CPU,您将是错误的。

2 - CPU 访问位于 N 的倍数地址的 N 字节数据类型有什么好处?

在上述 CPU 上,读取不是 N 的倍数(即对齐)的地址将导致段错误。段错误将导致进程终止。进程最好在完成它应该做的任何事情之后才终止。

在其他一些 CPU 上,从对齐地址访问内存会更快。越快越好。

可能在所有 CPU 上,访问未对齐的内存不会是原子操作。这是否更好或无关紧要取决于您在做什么。

3 - 为什么编译器不对成员重新排序以尽可能多地占用空间?我不确定是否有人使用实际的偏移量来访问成员。

因为语言保证了成员的顺序,所以无论您认为是否有人会这样做,程序员都可以依赖这种保证。有一些依赖它的罕见用例。

但是,对于程序员的保证不一定是成员任意顺序的唯一问题。另一个方面是库在不同编译器之间的兼容性。所有涉及的编译器都必须就成员的顺序达成一致。指定的顺序就是声明的顺序。

4 - 我读到这不再重要了,CPU 现在通常可以访问非对齐成员,花费与对齐成员相同的时间。真的吗?如果是,那为什么?

这是一个过于笼统的说法。对于某些 CPU,对于某些用例,这可能是正确的。我建议不要将这种笼统的陈述假设为普遍真理。

如果我们假设这对于特定 CPU 来说是正确的,其原因可能是这种新 CPU 可以访问未对齐的内存,而早期的 CPU 则不能(例如旧的 ARMv4)。

在另一种情况下,早期的 CPU 可能读取和写入未对齐,但此类操作可能会更慢。如果在较新的 CPU 上,这些操作具有相同的速度,那么对齐就变得不重要了。

较旧的 CPU 仍在使用中并没有消失。

【讨论】:

    【解决方案3】:
    1. 它们至少与 _Alignof(type) 对齐。原则上允许实现进一步对齐,但这通常是不可取的,并且没有主要实现这样做。

    2. 正如 Eljay 的评论中所述(强调我的):

      对齐是一个平台架构约束。与某些架构上的对齐访问一样,未对齐的数据访问可能代价高昂(性能成本高达 x16),并且可能阻碍原子读/写(仅与多线程应用程序相关),或不受支持完全(导致进程错误)。其他架构可以毫无问题地处理它们,而其他架构也可以处理它们,但会降低性能(因此编译器会在性能方面出错)。

      语言标准被编写为允许此类平台约束。

    3. 这是不允许的,至少如果结构的地址以一种使表示对应用程序可见的方式获取的话是不允许的。语言规范要求成员有序。这是 6.7.2.1 结构和联合说明符,¶15

      在结构对象中,非位域成员和位域所在的单元的地址按声明顺序递增。一个指向结构对象的指针,经过适当的转换,指向它的初始成员(或者如果该成员是位域,则指向它所在的单元),反之亦然。结构对象中可能有未命名的填充,但不是在其开头。

    4. 不,这不是真的。高端 cpu 通常修补不对齐的访问透明,以允许某些类型的草率代码以及必然不对齐的操作(例如 memcpymemmove 通过具有不同对齐方式的缓冲区),但是这并没有改变这样一个事实,即这些操作往往更昂贵,并且它们不适用于原子操作等某些事情。

    【讨论】:

      猜你喜欢
      • 2015-04-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-18
      • 1970-01-01
      • 2023-04-03
      • 2014-10-07
      • 2011-10-21
      相关资源
      最近更新 更多