我们的头文件使用#pragma pack(1)围绕我们的大多数结构体(用于网络和文件I/O)。我理解它将结构体的对齐从默认的8字节更改为1字节。假设所有东西都运行在32位Linux(也许Windows也是)中,这种打包对齐是否会对性能造成影响?我不关心库的可移植性,但更关心文件和网络I/O与不同#pragma包的兼容性,以及性能问题。
#pragma pack(1)
oyxsuwqo1#
当内存访问发生在字对齐的内存地址上时,访问速度最快。最简单的例子是下面的结构体(@Didier也使用了它):
struct sample { char a; int b; };
默认情况下,GCC会插入填充,因此a位于偏移量0,b位于偏移量4(字对齐)。如果没有填充,b就不会字对齐,访问速度会更慢。慢了多少?
require
关于便携性:我假设您使用#pragma pack(1),这样您就可以通过网络将结构体发送到磁盘或从磁盘发送结构体,而不必担心不同的编译器或平台对结构体的打包方式不同。
ccgok5k52#
是的。当然有。例如,如果您定义一个结构:
struct dumb { char c; int i; };
那么无论何时访问成员i,CPU都会变慢,因为32位值i不能以本机对齐的方式访问。为了简单起见,假设CPU必须从内存中获取3个字节,然后从下一个位置获取1个字节,以便将值从内存传输到CPU寄存器。
hc2pp10m3#
当你声明一个结构体时,大多数编译器都会在成员之间插入填充字节,以确保它们与内存中的适当地址对齐(通常填充字节是类型大小的倍数)。这使得编译器在访问这些成员时能够优化访问。#pragma pack(1)指示编译器使用特定的对齐方式打包结构成员。这里的1告诉编译器不要在成员之间插入任何填充。因此,是的,这肯定会导致性能下降,因为您强制编译器做一些超出其自然性能优化范围的事情。此外,一些平台要求对象在特定边界对齐,使用未对齐的结构可能会导致分段错误。理想情况下,最好避免更改默认的自然对齐规则。但是如果根本无法避免'pragma pack'指令(就像您的情况一样),则必须在定义需要紧密封装的结构后恢复原始封装方案。例如:
1
//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)
qv7cva1a4#
这取决于底层体系结构及其处理未对齐地址的方式。x86处理未对齐地址的方式很好,但会以性能为代价,而其他架构(如ARM)可能会调用对齐错误(SIGBUS),甚至将未对齐地址“舍入”到最近的边界,在这种情况下,您的代码将以一种可怕的方式失败。底线是,只有在您确信底层体系结构将处理未对齐的地址,并且网络I/O的成本高于处理成本时,才对它进行打包。
SIGBUS
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"
pbpqsu0x6#
从技术上讲,是的,它会影响性能,但仅限于内部处理。如果您需要为网络/文件IO打包结构,则需要在打包需求和内部处理之间取得平衡。我所说的内部处理是指在IO之间对数据所做的工作。如果您只做很少的处理,就不会在性能方面损失太多。否则,您可能希望在正确对齐的结构上进行内部处理,并且在执行IO时只“打包”结果。或者,您可以切换到只使用默认的对齐结构,但是您需要确保每个人都以相同的方式对齐它们(网络和文件客户端)。
wfypjpf47#
有些机器代码指令在32位或64位上运行(甚至更多),但希望数据在内存地址上对齐。如果它们不是这样,它们就必须在内存上执行一个以上的读/写周期才能执行任务。性能下降多少在很大程度上取决于您对数据的处理。如果构建大型结构体数组并对它们执行大量计算,那么它可能会变得很大;但如果只存储数据一次,只是为了在其他时间将其读回并转换为字节流,那么它可能几乎不会引起注意。
4dbbbstv8#
在某些平台上,如ARM Cortex-M0,如果在奇数地址上使用16位加载/存储指令,则会失败;如果在不是4的倍数的地址上使用32位指令,则会失败。从奇数地址加载或向奇数地址存储16位对象将需要使用三条指令,而不是一条;对于32位地址,将需要七个指令。在clang或gcc上,获取打包结构成员的地址将产生一个指针,该指针通常无法用于访问该成员。取__packed结构成员的地址将产生一个__packed限定指针,该指针只能存储在同样限定的指针对象中。通过此类指针进行的访问将使用多支持未对齐访问所需的指令序列。
__packed
fquxozlt9#
在今天的x86-CPU上,当你有一个不对齐的访问时,几乎没有性能差异。唯一的区别是当这个访问跨越了页面边界时。我写的代码只针对x86-CPU。所以当我有一个大的线性访问的数据结构时,其中的对象大小明显受益于#pragma pack(1),从而导致你有一个更密集的连续对象的 Package 。我使用pragma pack(1)。如果我将代码移植到没有快速非对齐访问的平台上,我可以使用#ifdef这个编译指示。有时这也适用于随机访问数据结构,取决于它们的大小。如果它们适合该高速缓存,那么字跨越两个缓存行的效果和多加载一个缓存行可能就不相关了。
pragma pack(1)
#ifdef
9条答案
按热度按时间oyxsuwqo1#
当内存访问发生在字对齐的内存地址上时,访问速度最快。最简单的例子是下面的结构体(@Didier也使用了它):
默认情况下,GCC会插入填充,因此a位于偏移量0,b位于偏移量4(字对齐)。如果没有填充,b就不会字对齐,访问速度会更慢。
慢了多少?
require
字对齐。关于便携性:我假设您使用
#pragma pack(1)
,这样您就可以通过网络将结构体发送到磁盘或从磁盘发送结构体,而不必担心不同的编译器或平台对结构体的打包方式不同。ccgok5k52#
是的。当然有。
例如,如果您定义一个结构:
那么无论何时访问成员i,CPU都会变慢,因为32位值i不能以本机对齐的方式访问。为了简单起见,假设CPU必须从内存中获取3个字节,然后从下一个位置获取1个字节,以便将值从内存传输到CPU寄存器。
hc2pp10m3#
当你声明一个结构体时,大多数编译器都会在成员之间插入填充字节,以确保它们与内存中的适当地址对齐(通常填充字节是类型大小的倍数)。这使得编译器在访问这些成员时能够优化访问。
#pragma pack(1)
指示编译器使用特定的对齐方式打包结构成员。这里的1
告诉编译器不要在成员之间插入任何填充。因此,是的,这肯定会导致性能下降,因为您强制编译器做一些超出其自然性能优化范围的事情。此外,一些平台要求对象在特定边界对齐,使用未对齐的结构可能会导致分段错误。
理想情况下,最好避免更改默认的自然对齐规则。但是如果根本无法避免'pragma pack'指令(就像您的情况一样),则必须在定义需要紧密封装的结构后恢复原始封装方案。
例如:
qv7cva1a4#
这取决于底层体系结构及其处理未对齐地址的方式。
x86处理未对齐地址的方式很好,但会以性能为代价,而其他架构(如ARM)可能会调用对齐错误(
SIGBUS
),甚至将未对齐地址“舍入”到最近的边界,在这种情况下,您的代码将以一种可怕的方式失败。底线是,只有在您确信底层体系结构将处理未对齐的地址,并且网络I/O的成本高于处理成本时,才对它进行打包。
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结构都可能未对齐,这对代码生成有重大影响,因为对成员的所有访问都必须处理地址未正确对齐的情况。
尽管结构S和P具有完全相同的布局,但是代码生成由于对准而不同。
注意到对于一些RISC处理器,代码大小爆炸是相当显著的。这可能反过来影响内联决策。
故事的寓意:除非绝对必要,否则不要将
#pragma pack(1)
应用于结构。它会使代码膨胀并抑制优化。#pragma pack(1)
and its variations are also subtly dangerous - even on x86 systems where they supposedly "work"pbpqsu0x6#
从技术上讲,是的,它会影响性能,但仅限于内部处理。如果您需要为网络/文件IO打包结构,则需要在打包需求和内部处理之间取得平衡。我所说的内部处理是指在IO之间对数据所做的工作。如果您只做很少的处理,就不会在性能方面损失太多。否则,您可能希望在正确对齐的结构上进行内部处理,并且在执行IO时只“打包”结果。或者,您可以切换到只使用默认的对齐结构,但是您需要确保每个人都以相同的方式对齐它们(网络和文件客户端)。
wfypjpf47#
有些机器代码指令在32位或64位上运行(甚至更多),但希望数据在内存地址上对齐。如果它们不是这样,它们就必须在内存上执行一个以上的读/写周期才能执行任务。性能下降多少在很大程度上取决于您对数据的处理。如果构建大型结构体数组并对它们执行大量计算,那么它可能会变得很大;但如果只存储数据一次,只是为了在其他时间将其读回并转换为字节流,那么它可能几乎不会引起注意。
4dbbbstv8#
在某些平台上,如ARM Cortex-M0,如果在奇数地址上使用16位加载/存储指令,则会失败;如果在不是4的倍数的地址上使用32位指令,则会失败。从奇数地址加载或向奇数地址存储16位对象将需要使用三条指令,而不是一条;对于32位地址,将需要七个指令。
在clang或gcc上,获取打包结构成员的地址将产生一个指针,该指针通常无法用于访问该成员。取
__packed
结构成员的地址将产生一个__packed
限定指针,该指针只能存储在同样限定的指针对象中。通过此类指针进行的访问将使用多支持未对齐访问所需的指令序列。fquxozlt9#
在今天的x86-CPU上,当你有一个不对齐的访问时,几乎没有性能差异。唯一的区别是当这个访问跨越了页面边界时。我写的代码只针对x86-CPU。所以当我有一个大的线性访问的数据结构时,其中的对象大小明显受益于
#pragma pack(1)
,从而导致你有一个更密集的连续对象的 Package 。我使用pragma pack(1)
。如果我将代码移植到没有快速非对齐访问的平台上,我可以使用#ifdef
这个编译指示。有时这也适用于随机访问数据结构,取决于它们的大小。如果它们适合该高速缓存,那么字跨越两个缓存行的效果和多加载一个缓存行可能就不相关了。