在c中打包结构是否有标准方法或标准替代方法?


13

当使用CI编程时,发现使用GCCs __attribute__((__packed__))属性打包结构非常有价值,因此我可以轻松地将易失性存储器的结构化块转换为字节数组,以通过总线传输,保存到存储或应用于寄存器块。打包的结构保证了当被当作字节数组对待时,它不会包含任何填充,这既浪费,可能的安全风险,又与接口硬件连接时不兼容。

没有适用于所有C编译器的打包结构的标准吗?如果不是,那么我认为这是系统编程的关键功能是一个异常现象吗?早期使用C语言的用户是否没有发现打包结构的需要,或者是否有其他选择?


在编译域中使用结构是一个非常糟糕的主意,特别是要指向硬件(这是另一个编译域)。数据包结构只是这样做的一个技巧,它们具有很多不良的副作用,因此,针对您的问题,还有许多其他解决方案,副作用更少,并且更易于移植。
old_timer '16

Answers:


12

在结构中,重要的是每个成员与每个结构实例的地址的偏移量。紧紧地包装东西的问题并不多。

但是,数组对如何“打包”很重要。C语言中的规则是,每个数组元素与上一个数组元素正好是N个字节,其中N是用于存储该类型的字节数。

但是有了结构,就没有这种对统一性的需求。

这是一个奇怪的打包方案的示例:

飞思卡尔(制造汽车微控制器的公司)制造的微型计算机具有时间处理单元协处理器(用于eTPU或TPU的Google)。它具有两个本机数据大小,分别为8位和24位,并且仅处理整数。

这个结构:

struct a
{
  U24 elementA;
  U24 elementB;
};

会看到每个U24存储了自己的32位块,但只存储在最高地址区域。

这个:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

将在相邻的32位块中存储两个U24,并将U8存储在第一个U24前面的“孔”中elementA

但是,如果需要,您可以告诉编译器将所有内容打包到其自己的32位块中。它在RAM上更昂贵,但使用的指令较少。

“打包”并不意味着“紧密打包”,它只是意味着一些用于安排带有偏移量的结构元素的方案。

没有通用方案,它取决于编译器+体系结构。


1
如果用于TPU的编译器重新安排struct belementC在其他任何元素之前移动,则它不是合格的C编译器。C中不允许元素重新排列
Bart van Ingen Schenau 2016年

有趣的是,但U24不是en.m.wikipedia.org/wiki/C_data_types的标准C类型,因此编译器被迫以某种奇怪的方式对其进行处理也就不足为奇了。
satur9nine

它与32位字长的主CPU内核共享RAM。但是该处理器具有仅处理24位或8位的ALU。因此,它有一个方案,可以用32位字分配24位数字。非标准,但包装和对齐的一个很好的例子。同意,这是非常不规范的。
RichColours

6

当使用CI进行编程时,发现使用GCC打包结构非常有价值__attribute__((__packed__))[...]

既然您提到了__attribute__((__packed__)),我假设您的意图是消除内的所有填充struct(使每个成员具有1字节的对齐方式)。

没有适用于所有C编译器的打包结构的标准吗?

...答案是“否”。相对于结构(以及堆栈或堆中结构的连续数组)的填充和数据对齐是一个重要原因。在许多计算机上,未对齐的内存访问可能会导致潜在的显着性能下降(尽管在某些较新的硬件上变得更少)。在某些极少数情况下,未对齐的内存访问会导致不可恢复的总线错误(甚至可能使整个操作系统崩溃)。

由于C标准着重于可移植性,因此没有一种标准的方法来消除结构中的所有填充并仅允许任意字段未对齐是没有意义的,因为这样做可能会导致C代码不可移植。

以消除所有填充的方式将此类数据输出到外部源的最安全,最可移植的方法是串行化到字节流/从字节流串行化,而不仅仅是尝试传递您的原始内存内容structs。这还可以防止您的程序在此序列化上下文之外遭受性能损失,并且还可以让您自由地将新字段添加到中,struct而不会影响整个软件。它还将为您提供一些空间来处理字节序以及类似的事情(如果有的话)。

有一种消除所有填充而不达到编译器特定指令的方法,尽管它仅适用于字段之间的相对顺序无关紧要的情况。给定这样的东西:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

...我们需要相对于包含这些字段的结构的地址来对齐内存访问的填充,如下所示:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

...其中.表示填充。每个组件都x必须与8字节边界对齐以提高性能(有时甚至是正确的行为)。

您可以通过使用SoA(数组结构)表示形式以一种可移植的方式消除填充(假设我们需要8个Foo实例):

struct Foos
{
   double x[8];
   char y[8];
};

我们已经有效地拆除了该结构。在这种情况下,内存表示如下所示:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... 还有这个:

01234567
yyyyyyyy

...不再增加填充开销,并且不会涉及未对齐的内存访问,因为我们不再将这些数据字段作为结构地址的偏移量访问,而是作为有效数组的基地址的偏移量访问。

这还带来了以下优点:由于要消耗较少的数据(混合中不再有无关紧要的填充来减慢计算机的相关数据消耗速率),因此顺序访问速度更快,并且还可能使编译器非常琐碎地向量化处理。

缺点是它是PITA编码。随机访问的效率可能较低,因为在字段之间跨度较大时,AoS或AoSoA代表通常会做得更好。但这是消除填充并尽可能紧密包装而不用拧紧所有部件对齐方式的一种标准方法。


2
我认为拥有一种明确指定结构布局的方法将大大提高可移植性。虽然某些布局会导致某些机器上的代码效率很高,而其他机器上的代码效率却很低,但这些代码将在所有机器上工作,并且至少在某些机器上会很高效。相比之下,在没有这种功能的情况下,使代码在所有计算机上运行的唯一方法可能是使其在所有计算机上效率低下,或者使用大量宏和条件编译来组合快速不可移植的代码程序和一个可移植的慢程序在同一源中。
超级猫

从概念上讲,是的,如果我们可以指定所有内容,包括位和字节表示形式,对齐要求,字节序等,并具有允许在C中进行此类显式控制,同时可以选择将其与基础体系结构进一步分离的功能……但是我只是在谈论ATM-当前串行器最可移植的解决方案是以一种不依赖于确切的位和字节表示以及数据类型对齐的方式编写它。不幸的是,我们缺少有效的ATM手段(在C中)。

5

并非所有架构都相同,只需打开一个模块上的32位选项,然后查看使用相同的源代码和相同的编译器时会发生什么。字节顺序是另一个众所周知的限制。使用浮点表示法,问题变得更加严重。使用打包发送二进制数据是不可移植的。要对其进行标准化以使其实际可用,您需要重新定义C语言规范。

尽管很常见,但是如果需要数据的安全性,可移植性或寿命,使用Pack发送二进制数据是个坏主意。您多久从源代码中读取一个二进制Blob到程序中。您多久检查一次所有值是否合理,以确保黑客或程序更改没有“获取”数据?在编写检查例程的代码时,您可能还需要使用导入和导出例程。


0

一个非常常见的替代方法是“命名填充”:

struct s {
  short s1;
  char  c2;
  char  reserved; // Padding
};

确实假定该结构不会被填充为8个字节。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.