C 结构中的自动字段重新排序以避免填充

Posted

技术标签:

【中文标题】C 结构中的自动字段重新排序以避免填充【英文标题】:Automated field re-ordering in C structs to avoid padding 【发布时间】:2010-10-26 10:21:50 【问题描述】:

我花了几分钟手动重新排序结构中的字段以减少填充效果[1],这感觉几分钟太多了。我的直觉告诉我,我最好把时间花在编写 Perl 脚本之类的东西上,而不是为我做这种优化。

我的问题是这是否也是多余的;是否已经有一些我不知道的工具,或者我应该能够打开[2] 来打包结构的一些编译器功能?

这个问题更加复杂,因为这需要在几个不同的架构上进行一致的优化,所以无论使用什么工具都需要能够考虑不同的结构对齐和指针大小。

编辑:快速澄清一下——我想做的是重新排序源代码中的字段以避免填充,而不是像在没有填充的情况下编译时那样“打包”结构。

编辑 #2:另一个复杂因素:根据配置,某些数据类型的大小也可能会发生变化。显而易见的是针对不同架构的指针和指针差异,还有浮点类型(16、32 或 64 位,取决于“精确度”)、校验和(8 位或 16 位,取决于“速度”)和一些其他不明显的东西。

[1] 所讨论的结构在嵌入式设备上被实例化了数千次,因此结构每减少 4 字节可能意味着 gono-go 之间的区别 这个项目。

[2] 可用的编译器有 GCC 3.* 和 4.*、Visual Studio、TCC、ARM ADS 1.2、RVCT 3.* 和其他一些比较模糊的编译器。

【问题讨论】:

这个结构的实例是否需要跨设备移植,或者每个架构都有自己的包装是否可以? 顺便说一句:我认为这是一个有趣的问题,并搜索了“perl struct reordering”。这是最高的结果。这个问题只有 15 分钟! Alnitak - 是的,这实际上是需要非常便携的代码 :) 每个架构都有自己的结构定义是可以的——但是编写特定于架构的定义是不切实际的手工。 【参考方案1】:

如果从存储中挤出的每个单词都很关键,那么我必须建议手动优化结构。一个工具可以为您安排最佳成员,但它不知道,例如,您存储在 16 位中的这个值实际上永远不会超过 1024,因此您可以为 窃取高 6 位这里的这个值...

因此,人类几乎肯定会在这项工作上击败机器人。

[编辑] 但是您似乎真的不想为每个架构手动优化您的结构。也许你真的有很多架构需要支持?

我确实认为这个问题不适用于通用解决方案,但您可以将您的领域知识编码到自定义 Perl/Python/something 脚本中,该脚本会为每个架构生成结构定义。

另外,如果您的所有成员的大小都是 2 的幂,那么您只需按大小对成员进行排序(最大的优先)即可获得最佳打包。在这种情况下,您可以使用良好的老式基于宏的结构-建设 - 像这样的东西:

#define MYSTRUCT_POINTERS      \
    Something*  m_pSomeThing;  \
    OtherThing* m_pOtherThing; 

#define MYSTRUCT_FLOATS        \
    FLOAT m_aFloat;            \
    FLOAT m_bFloat;

#if 64_BIT_POINTERS && 64_BIT_FLOATS
    #define MYSTRUCT_64_BIT_MEMBERS MYSTRUCT_POINTERS MYSTRUCT_FLOATS
#else if 64_BIT_POINTERS
    #define MYSTRUCT_64_BIT_MEMBERS MYSTRUCT_POINTERS
#else if 64_BIT_FLOATS
    #define MYSTRUCT_64_BIT_MEMBERS MYSTRUCT_FLOATS
#else
    #define MYSTRUCT_64_BIT_MEMBERS
#endif

// blah blah blah

struct MyStruct

    MYSTRUCT_64_BIT_MEMBERS
    MYSTRUCT_32_BIT_MEMBERS
    MYSTRUCT_16_BIT_MEMBERS
    MYSTRUCT_8_BIT_MEMBERS
;

【讨论】:

直到有人制造出更智能的机器人(完成这项工作)! 同意;这里涉及很多与上下文相关的知识。当然,如果您有大量的结构,并且您可以将所有这些知识嵌入到工具可以使用的格式中,那么就有可能实现自动化。 感谢您的回答。我有一个关于最佳排序的问题。在您的回答中,您提到最佳排序是从最大到最小排序。这个说法有什么证据吗?我已经尝试了很多案例,所有这些都不能破坏陈述,所以我想知道如何证明这一点。非常感谢。 从大到小排序通常不是最优的。一般情况是装箱问题,这是 NP 难的——如果你好奇的话,可以用谷歌搜索一些有趣的结果。只有在尺寸为 2 次方的特殊情况下,它才能完美包装,不留空隙;只看这个案例就很容易看出原因。每个 size-2^k 对象都以 2^k-aligned 边界结束,这也是 2^k-1-aligned 边界,因此“向下”到较小的尺寸永远不会导致间隙。 两个因素的最大幂 大小排​​序,决胜局原始位置确实是二进制计算机(阅读:现在几乎所有)可以做到的最好的位置特殊知识,因为所需和最佳对齐始终是 2 的幂和大小的除数。当然,如果共同的初始序列或第一个成员是相关的,则需要格外小心。【参考方案2】:

有一个名为 pstruct 的 Perl 脚本,它通常包含在 Perl 安装中。该脚本将转储结构成员的偏移量和大小。您可以修改 pstruct 或将其输出用作创建实用程序的起点,以您想要的方式打包您的结构。

$ cat foo.h 
struct foo 
    int x;
    char y; 
    int b[5];
    char c;
;

$ pstruct foo.h
struct foo 
  int                foo.x                      0       4
  char               foo.y                      4       1
                     foo.b                      8      20
  char               foo.c                     28       1

【讨论】:

好主意,但 pstruct 似乎有 C++ 问题:-(【参考方案3】:

大多数 C 编译器不会这样做,因为您可以做一些奇怪的事情(比如获取结构中元素的地址,然后使用指针魔法访问其余部分,绕过编译器)。一个著名的例子是 AmigaOS 中的双链表,它使用守护节点作为列表的头部和尾部(这使得在遍历列表时可以避免 ifs)。守护者头节点将始终具有pred == null,而尾节点将具有next == null,开发人员将这两个节点组合成一个三指针结构head_next null tail_pred。通过使用head_nextnull的地址作为头尾节点的地址,他们节省了四个字节和一个内存分配(因为他们只需要整个结构一次)。

因此,最好的办法可能是将结构编写为伪代码,然后编写一个预处理器脚本,从中创建真正的结构。

【讨论】:

没有 C 编译器会这样做,因为这会破坏规范,该规范要求结构的字段按照它们在结构中声明的顺序出现在内存中。 不想打破规格。 @unwind 默认情况下它没有完成,但旧版本的 gcc 可以选择 -fipa-struct-reorg 重新排序结构成员 ***.com/a/28780286/995714【参考方案4】:

看看#pragma pack。这改变了编译器对齐结构中元素的方式。您可以使用它来强制它们紧密排列在一起,没有空格。

See more details here

【讨论】:

默认情况下不打包结构,因为访问对齐的成员更有效。重新排序结构可以减小结构的大小,而不会实际破坏任何成员的对齐方式。 不是他要的...虽然这会给他最佳的包装。【参考方案5】:

这也取决于平台/编译器。如前所述,大多数编译器将所有内容都填充为 4 字节对齐(或更糟!),因此假设结构具有 2 个 short 和一个 long:

short
long
short

将占用 12 个字节(带有 2*2 字节的填充)。

重新排序

short
short
long

仍将占用 12 个字节,因为编译器将填充它以加快数据访问速度(这是大多数桌面的默认设置,因为它们更喜欢快速访问而不是内存使用)。您的嵌入式系统有不同的需求,因此无论如何您都必须使用#pragma pack。

至于重新排序的工具,我会简单地(手动)重新组织您的结构布局,以便将不同的类型放在一起。先把所有的短裤放进去,然后把所有的长头放进去,等等。如果你要完成包装,无论如何这就是工具会做的事情。您可能在类型之间的转换点中间有 2 个字节的填充,但我认为这不值得担心。

【讨论】:

并认为我删除了关于不同数据类型大小的答案!无论如何,如果将所有相同类型的字段放在一起,无论每个字段有多大,都将获得最佳打包。 我不确定“一切都以 4 字节对齐”;编译器将确保每个成员都满足其最小对齐要求。例如,如果 long double 需要 16 字节对齐,那么一个 char 后跟一个 long double 会留下一个 15 字节的空洞;但是一个 short 通常需要 2 字节对齐,一个 char 后跟一个 short 会留下一个 1 字节的空洞(而整体 - char,short - 后跟 long double 会留下一个 12 字节的空洞,但后跟一个 32 位的 int不会在 short 和 int 之间留下任何漏洞)。等等。 不,一个 char 后跟一个 long-double 通常会用完 1byte+3padbytes+16bytes。处理器线对齐的工作原理就是这样,因此可以在不进行任何位移的情况下提取字符,但您可以告诉它以不同的方式进行操作,对齐为 0 而不是 4,您的应用程序只会执行得更慢。您认为所有内容都需要按最大的个体类型对齐。【参考方案6】:

编译器不能通过自己的头部重新排序结构中的字段。标准要求字段应按定义的顺序排列。做其他事情可能会以微妙的方式破坏代码。

在您编写时,当然完全有可能制作某种代码生成器,以一种有效的方式在字段周围打乱。但我更喜欢手动操作。

【讨论】:

【参考方案7】:

考虑我将如何制作这样一个工具...我想我会从调试信息开始。

从源头获取每个结构的大小是一件很痛苦的事情。它与编译器已经完成的许多工作重叠。我对 ELF 不够熟悉,无法准确说明如何从调试二进制文件中提取结构大小信息,但我知道该信息存在,因为调试器可以显示它。也许 objdump 或 binutils 包中的其他东西可以轻松地为您获取这个(至少对于使用 ELF 的平台)。

获得信息后,剩下的就很简单了。将成员从大到小排序,尽量保持原始结构的顺序。使用 perl 或 python,甚至可以很容易地将其与源的其余部分进行交叉引用,甚至可以根据使用的干净程度保留 cmets 或 #ifdefs。最大的痛苦将是更改整个代码库中结构的所有初始化。哎呀。

事情就是这样。听起来很不错,但我不知道有任何这样的现有工具可以做到这一点,当你编写自己的......我想你已经能够手动重新排序大多数程序中的结构。

【讨论】:

【参考方案8】:

我有同样的问题。正如另一个答案中所建议的那样, pstruct 可能会有所帮助。但是,它确实提供了我们需要的东西。实际上 pstruct 使用 gcc 提供的调试信息。我基于相同的想法编写了另一个脚本。

您必须生成带有 STUBS 调试信息的程序集文件 (-gstubs)。 (可以从 dwarf 获得相同的信息,但我使用的方法与 pstruct 相同)。在不修改编译过程的情况下做到这一点的一个好方法是将"-gstubs -save-temps=obj" 添加到您的编译选项中。

以下脚本读取汇编文件并检测何时在结构中添加了额外的字节:

    #!/usr/bin/perl -n

    if (/.stabs[\t ]*"([^:]*):T[()0-9,]*=s([0-9]*)(.*),128,0,0,0/) 
       my $struct_name = $1;
       my $struct_size = $2;
       my $desc = $3;
       # Remove unused information from input
       $desc =~ s/=ar\([0-9,]*\);[0-9]*;[-0-9]*;\([-0-9,]*\)//g;
       $desc =~ s/=[a-zA-Z_0-9]+://g;
       $desc =~ s/=[\*f]?\([0-9,]*\)//g;
       $desc =~ s/:\([0-9,]*\)*//g;
       my @members = split /;/, $desc;
       my ($prev_size, $prev_offset, $prev_name) = (0, 0, "");
       for $i (@members) 
          my ($name, $offset, $size) = split /,/, $i;
          my $correct_offset = $prev_offset + $prev_size;
          if ($correct_offset < $offset) 
             my $diff = ($offset - $correct_offset) / 8;
             print "$struct_name.$name looks misplaced: $prev_offset + $prev_size = $correct_offset < $offset (diff = $diff bytes)\n";
          
          # Skip static members
          if ($offset != 0 || $size != 0) 
            ($prev_name, $prev_offset, $prev_size) = ($name, $offset, $size);
          
       
    

调用它的好方法:

find . -name *.s | xargs ./detectPaddedStructs.pl | sort | un

【讨论】:

以上是关于C 结构中的自动字段重新排序以避免填充的主要内容,如果未能解决你的问题,请参考以下文章

C中的结构内存布局

php Google地址自动填充字段重新排序#plugin #checkout

是否有允许结构重新排序的 GCC 关键字?

过滤和排序 SQL 查询以重新创建嵌套结构

删除字段时,Chrome 自动填充会重新填充输入

ggplot中的嵌套x标签:超过2,同时避免重新排序