5

我正在做一些示例程序来探索 C,并想知道为什么结构填充只能以 2 的幂来完成。

#include <stdio.h>

#pragma pack(push, 3)

union aaaa
{

   struct bbb
   {
      int a;
      double b;
      char c;
   }xx;

   float f;
};

#pragma pack(pop)

int main()
{

printf("\n Size: %d", sizeof(union aaaa));

return 0;
}

编译时

warning: alignment must be a small power of two, not 3 [-Wpragmas]
warning: #pragma pack (pop) encountered without matching #pragma pack (push) [-Wpragmas]

似乎#pragma 没有效果。输出仅为 24。即4字节对齐。

4

5 回答 5

21

简短的回答是,处理器中的基本对象的大小是 2 的小幂(例如,1、2、4、8 和 16 字节),并且内存按大小是 2 的小幂(例如,8字节),因此必须对齐结构才能很好地适应这些大小。

长答案是,其原因是基于物理学和初等数学。计算机自然而然地使用值为 0 和 1 的位。这是因为很容易设计在两个值之间切换的物理事物:高电压和低电压,是否存在电荷,等等。区分三个值更难,因为您必须对值之间的转换更加敏感。因此,随着计算机技术几十年来的发展,我们使用了比特(二进制数字)而不是像三进制数字这样的替代品。

为了产生更大的数字,我们组合了多个位。所以两位可以结合起来有四个值。三位可以有八个值,依此类推。在较旧的计算机中,有时位一次被分组为 6 个或 10 个。然而,八个变得普遍,并且现在基本上是标准的。使用 8 位作为一个字节并不像我描述的其他一些分组那样有很强的物理原因,但它是世界的方式。

计算机的另一个特点是内存。一旦我们有了这些字节,我们就想将它们中的很多存储在一个处理器可以轻松访问的设备中,这样我们就可以快速地将大量字节输入和输出处理器。当我们有很多字节时,我们需要一种方法让处理器告诉内存处理器想要读取或写入哪些字节。所以处理器需要一种方法来寻址字节。

处理器使用位来表示值,因此它将使用位来表示地址值。因此,内存将被构建为接受位,以指示在处理器读取时向处理器提供哪些字节,或者在处理器写入时存储哪些字节。存储设备如何处理这些位?一件简单的事情是使用一个位来控制通往记忆的路径的一个开关。内存将由许多存储字节的小部分组成。

考虑内存设备中可以存储一个字节的东西,并考虑其中两个彼此相邻的东西,比如 A 和 B。我们可以使用开关来选择是希望 A 字节处于活动状态还是 B 字节处于活动状态积极点。现在考虑其中的四个,比如 A、B、C 和 D。我们可以使用一个开关来选择是使用 AB 组还是使用 CD 组。然后另一个开关选择 A 或 B(如果使用 AB 组)或 C 或 D(如果使用 CD)组。

这个过程继续:内存地址中的每个位选择一组要使用的存储单元。1位选择2个存储单元,2位选择4个,3位选择8个,4位选择16个,以此类推。8位选择256个存储单元,24位选择16,777,216个存储单元,32位选择4,294,967,296个存储单元。

还有一种复杂情况。在处理器和内存之间移动单个字节很慢。相反,现代计算机将内存组织成更大的部分,例如 8 个字节。您一次只能在内存和处理器之间移动八个字节。当处理器请求内存提供一些数据时,处理器只发送地址的高位——低三位选择八个字节内的单个字节,它们不发送到内存。

这更快,因为处理器在其他情况下让内存完成所有切换以提供一个字节所需的时间获得八个字节,并且它更便宜,因为您不需要大量额外的开关来区分个体内存中的字节。

但是,现在这意味着处理器无法从内存中获取单个字节。当您执行访问单个字节的指令时,处理器必须从内存中读取八个字节,然后在处理器内部移动这些字节以获得您想要的一个字节。同样,为了获得两个或四个字节,处理器读取八个字节并仅提取您想要的字节。

为了简化这个过程,处理器设计人员指定数据应该以某种方式对齐。通常,它们需要将 2 字节数据(如 16 位整数)与 2 字节的倍数对齐,将 4 字节数据(如 32 位整数和 32 位浮点值)与 4 的倍数对齐字节和八字节数据对齐为八字节的倍数。

这种所需的对齐有两个效果。首先,由于四字节数据只能从内存读取的八字节块中的两个位置开始(开头或中间),处理器设计人员只需插入电线即可从两个位置提取四个字节。他们不需要添加所有额外的线来从八个单独的字节中的任何一个中提取四个字节,如果允许任何对齐,这些字节可能是起始位置。(一些处理器将完全禁止加载未对齐的数据,而一些处理器将允许它,但使用缓慢的方法来提取它,使用更少的线路但使用迭代算法在多个处理器周期内移动数据,因此未对齐的加载很慢。)

第二个影响是,因为四字节数据只能从八字节块中的两个位置开始,它也在该块内结束。考虑一下如果您尝试加载从 8 字节块的第 6 个字节开始的 4 字节数据会发生什么。前两个字节在块中,但接下来的两个字节在内存中的下一个块中。处理器必须从内存中读取两个块,从每个块中获取不同的字节,然后将这些字节放在一起。这比只读取一个块要慢得多。

因此,内存是按 2 的幂来组织的,因为这是位的自然结果,而处理器需要对齐,因为这样可以提高内存访问的效率。对齐自然是 2 的幂,这就是为什么当结构大小是用于对齐的 2 的幂的倍数时,结构尺寸会更好地工作。

于 2012-07-26T12:25:05.230 回答
8

因为否则就没有意义。您向结构添加填充是因为 CPU 在对齐数据上的工作速度更快(并且,在某些架构上,它们根本不适用于未对齐的数据),并且各种数据类型的对齐要求始终是 2 的小幂(至少,在我听说过的任何架构上)。

尽管如此,如果出于某种奇怪的原因您确实需要任意对齐,那么没有什么可以阻止您char在正确的位置添加虚拟数组来强制对齐(这或多或少是编译器在后台所做的)。

于 2012-07-26T10:21:40.133 回答
3

内存总线只有这么多字节宽,通常是两个字节宽的幂,因为这是字段中位的最大有效使用

像这样的三位字段

[0][0][0]

有八个数字的可能表示

0, 1, 2, 3, 4, 5, 6, 7, and 8

如果你把自己限制在数字上

0, 1, 2

那么您将浪费最高位,该位始终为零。计算早期的硬件和软件设计人员需要他们可以抓住的每一点,因为内存非常昂贵,所以这种浪费是在系统之外设计的。

后来,随着内存子系统的增长,对齐访问的设计成本变得更低。对齐访问要求数据元素的开始位于某些边界上,以减少跨内存总线的传输次数,并减少总线管理中的计算次数。

“二的幂”要求是总线架构加上一个简单的例程的副作用,该例程确保 C 数据结构可以与对齐的访问边界对齐。

于 2012-07-26T15:46:42.167 回答
2

虽然计算机设计有利于内存的二次幂内存对齐的物理原因,但另一个同样重要的要求是所有内存对齐必须均匀地划分为某个数字。否则,例如,如果一个结构需要在 7 的倍数上对齐,一个需要在 8 的倍数上对齐,一个需要在 9 的倍数上对齐,则union包含所有三个结构的 a 必须对齐到504 的倍数。

处理可分性要求的正常方式是说所有对齐大小必须细分为 2 的某个较大的幂。这种方法会奏效,但它不是唯一可行的实施方案。如果有人愿意的话,可以设计能够与 120 字节缓存线一起工作的硬件,并允许对象在 2、3、4、5、6、8、10、12、15、20 的倍数上对齐、24、30、40、60 或 120 字节。从硬件的角度来看,这样的设计实际上是非常合理的(每个高速缓存行将被存储为 1024 位,包括 64 位的纠错、写入标记或其他信息),并且将允许有效存储 80 位实数, RGB 或 XYZ 三元组(任何数字类型,包括 80 位实数)。如果不要求物理地址与逻辑地址的数字顺序相同,将 120 字节范围映射到缓存行所需的电路不会过于昂贵。另一方面,在专业应用程序之外,非二次方映射的好处不太可能足以克服成本和市场惯性问题。

于 2012-07-26T15:37:30.033 回答
0

根据 GCC 文档(http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html),pack pragma 用于“与 Microsoft Windows 编译器兼容”。

如果您搜索有关对齐的 MS 文档 ( http://msdn.microsoft.com/en-us/library/ms253949(v=vs.80).aspx ),您会发现 MSVC 编译器确保基于数据的类型对齐尺寸; 正如其他帖子所解释的那样,从逻辑上讲,它始终是 2 的幂。

如果您的问题是您需要对数据结构进行一些花哨的对齐(以访问一些考虑不周的内存映射外围设备),那么您最好的办法是通过添加添加所需填充的字段来使用手动打包结构。

于 2012-07-26T13:44:23.660 回答