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

dluptydi  于 2022-11-24  发布在  其他
关注(0)|答案(9)|浏览(308)

我们的头文件使用#pragma pack(1)围绕我们的大多数结构体(用于网络和文件I/O)。我理解它将结构体的对齐从默认的8字节更改为1字节。
假设所有东西都运行在32位Linux(也许Windows也是)中,这种打包对齐是否会对性能造成影响?
我不关心库的可移植性,但更关心文件和网络I/O与不同#pragma包的兼容性,以及性能问题。

oyxsuwqo

oyxsuwqo1#

当内存访问发生在字对齐的内存地址上时,访问速度最快。最简单的例子是下面的结构体(@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字节边界的字或双字操作数或跨越8字节边界的四字操作数被视为未对齐,需要两个单独的内存总线周期才能访问。与大多数性能问题一样,您必须对应用进行基准测试,以了解实际中这一问题的严重程度。
  • 根据维基百科,x86扩展像SSE2 require字对齐。
  • 许多其他架构需要字对齐(如果数据结构没有字对齐,则会产生SIGBUS错误)。

关于便携性:我假设您使用#pragma pack(1),这样您就可以通过网络将结构体发送到磁盘或从磁盘发送结构体,而不必担心不同的编译器或平台对结构体的打包方式不同。

  • 这并不能处理big endian和little endian的问题,你可以通过调用htons系列的函数来处理这些问题。
  • 根据我的经验,在应用程序代码中使用打包的可序列化结构体并不是一件很有趣的事情。在不破坏向后兼容性的情况下,很难修改和扩展这些结构体,而且正如已经提到的,这会对性能造成影响。考虑将打包的可序列化结构体的内容转换为等效的非打包的可扩展结构体进行处理。或者考虑使用成熟的串行化库,如Protocol Buffers(它具有C bindings)。
ccgok5k5

ccgok5k52#

是的。当然有。
例如,如果您定义一个结构:

struct dumb {
    char c;
    int  i;
};

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

hc2pp10m

hc2pp10m3#

当你声明一个结构体时,大多数编译器都会在成员之间插入填充字节,以确保它们与内存中的适当地址对齐(通常填充字节是类型大小的倍数)。这使得编译器在访问这些成员时能够优化访问。
#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)
qv7cva1a

qv7cva1a4#

这取决于底层体系结构及其处理未对齐地址的方式。
x86处理未对齐地址的方式很好,但会以性能为代价,而其他架构(如ARM)可能会调用对齐错误(SIGBUS),甚至将未对齐地址“舍入”到最近的边界,在这种情况下,您的代码将以一种可怕的方式失败。
底线是,只有在您确信底层体系结构将处理未对齐的地址,并且网络I/O的成本高于处理成本时,才对它进行打包。

qpgpyjmq

qpgpyjmq5#

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

当然。2020年1月,微软的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"

pbpqsu0x

pbpqsu0x6#

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

wfypjpf4

wfypjpf47#

有些机器代码指令在32位或64位上运行(甚至更多),但希望数据在内存地址上对齐。如果它们不是这样,它们就必须在内存上执行一个以上的读/写周期才能执行任务。性能下降多少在很大程度上取决于您对数据的处理。如果构建大型结构体数组并对它们执行大量计算,那么它可能会变得很大;但如果只存储数据一次,只是为了在其他时间将其读回并转换为字节流,那么它可能几乎不会引起注意。

4dbbbstv

4dbbbstv8#

在某些平台上,如ARM Cortex-M0,如果在奇数地址上使用16位加载/存储指令,则会失败;如果在不是4的倍数的地址上使用32位指令,则会失败。从奇数地址加载或向奇数地址存储16位对象将需要使用三条指令,而不是一条;对于32位地址,将需要七个指令。
在clang或gcc上,获取打包结构成员的地址将产生一个指针,该指针通常无法用于访问该成员。取__packed结构成员的地址将产生一个__packed限定指针,该指针只能存储在同样限定的指针对象中。通过此类指针进行的访问将使用多支持未对齐访问所需的指令序列。

fquxozlt

fquxozlt9#

在今天的x86-CPU上,当你有一个不对齐的访问时,几乎没有性能差异。唯一的区别是当这个访问跨越了页面边界时。我写的代码只针对x86-CPU。所以当我有一个大的线性访问的数据结构时,其中的对象大小明显受益于#pragma pack(1),从而导致你有一个更密集的连续对象的 Package 。我使用pragma pack(1)。如果我将代码移植到没有快速非对齐访问的平台上,我可以使用#ifdef这个编译指示。有时这也适用于随机访问数据结构,取决于它们的大小。如果它们适合该高速缓存,那么字跨越两个缓存行的效果和多加载一个缓存行可能就不相关了。

相关问题