为什么填充必须是2的幂?

时间:2012-07-26 10:15:48

标签: c struct

我正在做一些示例程序来探索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字节对齐。

5 个答案:

答案 0 :(得分:16)

简短的回答是处理器中的基本对象具有两个小功率的大小(例如,1,2,4,8和16字节),并且存储器被组织成大小为2的小功率的组(例如,8个字节),因此必须对齐结构以适应这些尺寸。

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

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

计算机的另一个特性是内存。一旦我们拥有这些字节,我们就希望将它们存储在一个易于处理器访问的设备中,这样我们就可以快速地将大量字节输入和输出处理器。当我们有很多字节时,我们需要一种方法让处理器告诉内存处理器想要读或写的字节。因此处理器需要一种方法来处理字节。

处理器使用位作为值,因此它将使用位作为地址值。因此,内存将被构建为接受位以指示当处理器读取时要向处理器提供哪些字节或者当处理器写入时要存储哪些字节。存储器设备对这些位做了什么?一个简单的方法是使用一位来控制通向存储器的路径的一个开关。内存将由许多存储字节的小部分组成。

考虑一下存储设备中可以存储一个字节的东西,并考虑其中两个相邻的东西,比如说A和B.我们可以使用一个开关来选择我们是否希望A字节是活动的还是B字节有效。现在考虑其中的四个,比如A,B,C和D.我们可以使用一个开关来选择是使用A-B组还是使用C-D组。然后另一个开关选择A或B(如果使用A-B组)或C或D(如果使用C-D)组。

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

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

这更快,因为处理器在内存完成所有切换以提供一个字节所需的时间内获得8个字节,并且它更便宜,因为您不需要大量额外的交换机区分内存中的各个字节。

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

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

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

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

因此,内存由2的幂组织,因为这是位的自然结果,并且处理器需要对齐,因为这使得内存访问更有效。对齐自然是2的幂,这就是为什么当它们是用于对齐的2的幂的倍数时,你的结构尺寸更好。

答案 1 :(得分:8)

因为否则就没有意义了。您可以为结构添加填充,因为CPU在对齐数据上工作得更快(在某些体系结构上,它们根本不能在未对齐的数据上工作),并且各种数据类型的对齐必需条件总是小于2的幂(至少,在我听说的任何架构上。)

但是,如果由于一些奇怪的原因你需要任意对齐,没有什么可以阻止你在正确的位置添加虚拟char数组来强制你的对齐(这或多或少是编译器在引擎盖下做的事情) )。

答案 2 :(得分:3)

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

像这样的三位字段

[0][0][0]

可能代表八个数字

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

如果你限制自己的数字

0, 1, 2

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

后来,当内存子系统增长时,对齐访问变得更便宜。对齐访问要求数据元素的启动在某些边界上,以减少跨内存总线的传输次数,并减少总线管理中的计算次数。

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

答案 3 :(得分:2)

虽然有些物理原因导致计算机设计偏向于内存两个内存对齐,但另一个同样重要的要求是所有内存对齐必须均匀分配到某个数字。否则,如果是一个结构需要在7的倍数上对齐,一个需要在8的倍数上对齐,一个需要在9的倍数上对齐,包含所有三个结构的union必须对齐到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字节范围映射到高速缓存行所需的电路不会过于昂贵。另一方面,在专门应用之外,非二次幂映射的好处不足以克服成本和市场惯性问题。

答案 4 :(得分:0)

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

如果在对齐(http://msdn.microsoft.com/en-us/library/ms253949(v=vs.80).aspx)上搜索MS文档,您会发现MSVC编译器根据数据大小确保类型对齐;正如其他帖子所解释的那样,逻辑上始终是2的力量。

如果您的问题是需要对数据结构进行一些精确对齐(访问一些不良内存映射外设),那么最好的方法是使用手动包装结构添加所需填充的字段。