使用pragma pack(1)时是否存在性能问题?

时间:2011-10-17 12:08:51

标签: c gcc

我们的标头在大多数结构周围使用#pragma pack(1)(用于网络和文件I / O)。据我所知,它将结构的对齐方式从默认的8个字节更改为1个字节的对齐方式。

假设所有内容都在32位Linux(也许是Windows)中运行,那么这种打包对齐是否有任何性能损失?

我不关心库的可移植性,但更关注文件和网络I / O与不同#pragma包的兼容性以及性能问题。

8 个答案:

答案 0 :(得分:14)

当字符对齐的内存地址发生时,内存访问速度最快。最简单的例子是以下结构(@Didier也使用过):

struct sample {
   char a;
   int b;
};

默认情况下,GCC插入填充,因此a位于偏移0处,b位于偏移4处(字对齐)。没有填充,b不是字对齐的,访问速度较慢。

慢多少?

  • 对于32位x86,根据Intel 64 and IA32 Architectures Software Developer's Manual
    处理器需要两个内存 访问以进行未对齐的内存访问;对齐访问只需要一个 内存访问。跨越4字节边界或a的字或双字操作数 跨越8字节边界的四字操作数被认为是未对齐的 需要两个独立的内存总线周期才能访问。
    与大多数性能问题一样,您必须对应用程序进行基准测试,以了解这在实践中存在多少问题。
  • 根据Wikipedia,像SSE2 这样的x86扩展需要字对齐。
  • 许多其他架构需要字对齐(如果数据结构不是字对齐的话,会产生SIGBUS错误。)

关于可移植性:我假设您正在使用#pragma pack(1),以便您可以通过线路和磁盘发送结构,而不必担心不同的编译器或平台打包结构。这是有效的,但是,要记住几个问题:

  • 这对处理大端和小端问题没有任何作用。您可以通过在结构中的任何整数,无符号等上调用htons函数族来处理这些函数。
  • 根据我的经验,在应用程序代码中使用打包的可序列化结构并不是很有趣。它们很难在不破坏向后兼容性的情况下进行修改和扩展,并且如前所述,存在性能损失。考虑将打包的,可序列化的结构内容转换为等效的非打包,可扩展的结构进行处理,或者考虑使用像Protocol Buffers这样的完整序列化库(具有C bindings)。

答案 1 :(得分:6)

是。绝对有。

例如,如果定义结构:

struct dumb {
    char c;
    int  i;
};

然后无论何时访问成员i,CPU都会变慢,因为无法以原生的对齐方式访问32位值。为简单起见,假设CPU必须从内存中获取3个字节,然后从下一个位置获取另外1个字节,以将值从内存传输到CPU寄存器。

答案 2 :(得分:3)

当声明结构时,大多数编译器在成员之间插入填充字节以确保它们与内存中的适当地址对齐(通常填充字节是类型大小的倍数)。这使得编译器能够在访问这些成员时具有优化的访问权限。

#pragma pack(1)指示编译器打包具有特定对齐的结构成员。这里的1告诉编译器不要在成员之间插入任何填充。

所以肯定存在明显的性能损失,因为你强迫编译器做一些超出它自然会对性能优化做的事情。还有,某些平台要求对象被对齐在特定的边界和使用未加数的结构可能会给你分段错误。

理想情况下,最好避免更改默认的自然对齐规则。但是如果根本无法避免'pragma pack'指令(如你的情况那样),那么在定义需要紧密打包的结构之后,必须恢复原包装方案。

例如:

//push current alignment rules to internal stack and force 1-byte alignment boundary
#pragma pack(push,1)  

/*   definition of structures that require tight packing go in here   */

//restore original alignment rules from stack    
#pragma pack(pop)

答案 3 :(得分:2)

这取决于底层架构及其处理未对齐地址的方式。

x86优雅地处理未对齐的地址,但性能成本较高,而ARM等其他体系结构可能会调用对齐错误(SIGBUS),甚至将未对齐的地址“舍入”到最近的边界,在这种情况下你的代码会以一种可怕的方式失败。

最重要的是,只有当您确定底层架构将处理未对齐的地址,并且网络I / O的成本高于处理成本时才打包它。

答案 4 :(得分:1)

使用编译指示包(1)时是否存在性能问题?

绝对。 2020年1月,Microsoft的Raymond Chen发布了一些具体示例,说明如何使用#pragma pack(1)来生成肿的可执行文件,这些可执行文件需要很多很多指令来对打包结构执行操作。尤其是在不直接支持硬件中未对齐访问的非x86硬件上。

Anybody who writes #pragma pack(1) may as well just wear a sign on their forehead that says “I hate RISC”

使用#pragma pack(1)时,这会将默认结构打包更改为字节打包,删除通常插入的所有填充字节以保持对齐。

...

任何P结构可能未对齐的可能性都会对代码生成产生重大影响,因为对成员的所有访问都必须处理地址未正确对齐的情况。

void UpdateS(S* s)
{
 s->total = s->a + s->b;
}

void UpdateP(P* p)
{
 p->total = p->a + p->b;
}

尽管结构S和P具有完全相同的布局, 由于对齐,代码生成有所不同。

UpdateS                       UpdateP
Intel Itanium

adds  r31 = r32, 4            adds  r31 = r32, 4
adds  r30 = r32  8 ;;         adds  r30 = r32  8 ;;
ld4   r31 = [r31]             ld1   r29 = [r31], 1
ld4   r30 = [r30] ;;          ld1   r28 = [r30], 1 ;;
                              ld1   r27 = [r31], 1
                              ld1   r26 = [r30], 1 ;;
                              dep   r29 = r27, r29, 8, 8
                              dep   r28 = r26, r28, 8, 8
                              ld1   r25 = [r31], 1
                              ld1   r24 = [r30], 1 ;;
                              dep   r29 = r25, r29, 16, 8
                              dep   r28 = r24, r28, 16, 8
                              ld1   r27 = [r31]
                              ld1   r26 = [r30] ;;
                              dep   r29 = r27, r29, 24, 8
                              dep   r28 = r26, r28, 24, 8 ;;
add   r31 = r30, r31 ;;       add   r31 = r28, r29 ;;
st4   [r32] = r31             st1   [r32] = r31
                              adds  r30 = r32, 1
                              adds  r29 = r32, 2 
                              extr  r28 = r31, 8, 8
                              extr  r27 = r31, 16, 8 ;;
                              st1   [r30] = r28
                              st1   [r29] = r27, 1
                              extr  r26 = r31, 24, 8 ;;
                              st1   [r29] = r26
br.ret.sptk.many rp           br.ret.sptk.many.rp

...
[examples from other hardware]
...

请注意,对于某些RISC处理器,代码大小的爆炸非常重要。反过来,这可能会影响内联决策。

故事的寓意:除非绝对必要,否则请勿将#pragma pack(1)应用于建筑物。它会使您的代码肿,并禁止优化。

#pragma pack(1) and its variations are also subtly dangerous - even on x86 systems where they supposedly "work"

答案 5 :(得分:0)

从技术上讲,是的,它会影响性能,但仅限于内部处理。如果您需要为网络/文件IO打包的结构,则在打包要求和内部处理之间存在平衡。通过内部处理,我的意思是,你对IO之间的数据所做的工作。如果你只进行很少的处理,你就不会在性能方面损失太多。否则,您可能希望对正确对齐的结构进行内部处理,并且只在执行IO时“打包”结果。或者您可以切换到仅使用默认对齐结构,但您需要确保每个人以相同的方式对齐它们(网络和文件客户端)。

答案 6 :(得分:0)

某些机器代码指令在32位或64位(甚至更多)上运行,但希望数据在内存地址上对齐。如果他们不是,他们必须在内存上执行多个读/写cyce来执行他们的任务。 性能达到的程度在很大程度上取决于您对数据的处理方式。如果构建大型结构数组并对它们执行大量计算,它可能会变大。但是,如果您只将数据存储一次,只是为了在其他时间将数据转换回字节流,那么它可能几乎不可能。

答案 7 :(得分:0)

在某些平台(例如ARM Cortex-M0)上,如果在奇数地址上使用16位加载/存储指令将失败,而在非4倍数的地址上使用32位指令将失败。从一个可能是奇数的地址装载或存储一个16位对象可能需要使用三条指令,而不是一条指令。对于32位地址,将需要7条指令。

在clang或gcc上,采用打包结构成员的地址将产生一个指针,该指针通常无法用于访问该成员。在更有用的Keil编译器上,采用__packed结构成员的地址将产生__packed限定指针,该指针只能存储在同样限定的指针对象中。通过此类指针进行的访问将使用支持不对齐访问所必需的多指令序列。