【问题标题】:How can I clear the padding bytes in struct for comparison?如何清除 struct 中的填充字节以进行比较?
【发布时间】:2019-04-20 20:51:07
【问题描述】:

我有一个定义如下的结构:

struct s_zoneData {
    bool finep = true;
    double pzone_tcp = 1.0;
    double pzone_ori = 1.0;
    double pzone_eax = 1.0;
    double zone_ori  = 0.1;
    double zone_leax = 1.0;
    double zone_reax = 0.1;
};

我创建了一个比较运算符:

bool operator==(struct s_zoneData i, struct s_zoneData j) {

    return (memcmp(&i, &j, sizeof(struct s_zoneData)) == 0);

}

大多数情况下,比较失败,即使是相同的变量。我花了一些时间(并且弄乱了 gdb)才意识到问题在于 finep 结构元素的填充字节是未初始化的垃圾。作为参考,在我的机器 (x64) 中,sizeof(struct s_zoneData) 是 56,这意味着 finep 元素有 7 个填充字节。

起初,我解决了将 memcmp 替换为结构每个成员的基于 ULP 的浮点值比较的问题,因为我认为可能存在舍入问题。但现在我想更深入地研究这个问题,看看可能的替代解决方案。

问题是,有没有办法为不同的编译器和平台指定填充字节的值?或者,将其重写为更一般的问题,因为我可能过于专注于我的方法,比较两个 struct s_zoneData 变量的正确方法是什么?

我知道创建一个虚拟变量(例如char pad[7])并用零初始化它应该可以解决问题(至少对于我的特定情况),但我读过多种情况,人们在不同的编译器和会员顺序,所以我更愿意使用标准定义的解决方案,如果存在的话。或者至少,保证对不同平台和编译器的兼容性。

【问题讨论】:

  • 那其实不太好。不仅因为填充(只能通过使用例如memset“清除”),还因为它是比较浮点值的一种非常糟糕的方法。事实上,您甚至不应该使用== 来比较浮点值,因为两个看似相等的值实际上可能(由于计算中的复合舍入误差)。要比较浮点值,请检查值之间的差异是否小于特定的 epsilon
  • 这就是为什么我用基于 ULP 的浮点比较替换了 memcmp,对于这种特殊情况。问题与填充字节有关,特别是如何避免使用memcmp 比较两个相同的结构时失败,或者比较它们的最佳方法是什么。

标签: c++ struct padding


【解决方案1】:

将填充字节设置为可预测值的唯一方法是使用memset 将整个结构设置为可预测的值——如果您总是在将字段设置为其他值之前使用 memset 清除结构的值,那么即使复制整个结构(如将其作为参数传递时),您也可以依靠填充字节保持不变。此外,具有静态存储持续时间的变量会将填充字节初始化为 0。

【讨论】:

  • 没有这样的保证;分配给任何结构成员或复制结构会使任何填充字节变得未指定
【解决方案2】:

虽然对于 c 或汇编程序员(实际上还有许多 c++ 程序员)来说,您所做的事情似乎是合乎逻辑的,但您无意中所做的是破坏了 c++ 对象模型并调用了未定义的行为。

您可能需要考虑根据对其数据成员的引用元组来比较值类型。

比较两个这样的元组会产生正确的比较行为以及相等性。

它们的优化也非常好。

例如:

#include <tuple>

struct s_zoneData {
    bool finep = true;
    double pzone_tcp = 1.0;
    double pzone_ori = 1.0;
    double pzone_eax = 1.0;
    double zone_ori  = 0.1;
    double zone_leax = 1.0;
    double zone_reax = 0.1;

    friend auto as_tuple(s_zoneData const & z)
    {
        using std::tie;
        return tie(z.finep, z.pzone_tcp, z.pzone_ori, z.pzone_eax, z.zone_ori, z.zone_leax, z.zone_reax);
    }
};

auto operator ==(s_zoneData const& l, s_zoneData const& r) -> bool
{
    return as_tuple(l) == as_tuple(r);
}

示例汇编器输出:

operator==(s_zoneData const&, s_zoneData const&):
  xor eax, eax
  movzx ecx, BYTE PTR [rsi]
  cmp BYTE PTR [rdi], cl
  je .L20
  ret
.L20:
  movsd xmm0, QWORD PTR [rdi+8]
  ucomisd xmm0, QWORD PTR [rsi+8]
  jp .L13
  jne .L13
  movsd xmm0, QWORD PTR [rdi+16]
  ucomisd xmm0, QWORD PTR [rsi+16]
  jp .L13
  jne .L13
  movsd xmm0, QWORD PTR [rdi+24]
  ucomisd xmm0, QWORD PTR [rsi+24]
  jp .L13
  jne .L13
  movsd xmm0, QWORD PTR [rdi+32]
  ucomisd xmm0, QWORD PTR [rsi+32]
  jp .L13
  jne .L13
  movsd xmm0, QWORD PTR [rdi+40]
  ucomisd xmm0, QWORD PTR [rsi+40]
  jp .L13
  jne .L13
  movsd xmm0, QWORD PTR [rdi+48]
  ucomisd xmm0, QWORD PTR [rsi+48]
  mov edx, 0
  setnp al
  cmovne eax, edx
  ret
.L13:
  xor eax, eax
  ret

【讨论】:

  • 这看起来不错,谢谢。我不知道tupletie。我会读到他们。同时,这种方法是否使用成员类型的 == 运算符比较结构成员,以防它们不是原语?有没有办法分配一个比较函数?
  • @Guille 它对元组中的每种类型都正确使用了 operator== 。 en.cppreference.com/w/cpp/utility/tuple/operator_cmp
【解决方案3】:
  1. #pragma pack 可以去掉多余的填充。
  2. 您可以通过手动添加来防止额外的填充添加,以便可以将其显式设置为预定义的值(但必须在结构之外进行初始化):


struct s_zoneData {
    char pad[sizeof(double)-sizeof(bool)];
    bool finep;
    double pzone_tcp;
    double pzone_ori;
    double pzone_eax;
    double zone_ori;
    double zone_leax;
    double zone_reax;
};

...
s_zoneData X = {{},true, 1.0, 1.0, 0.1, 1.0, 0.1};

编辑:根据@Guille 评论,填充应与 bool 成员结合使用,以防止内部填充。因此,要么焊盘应该紧挨在finep 之前/之后(我将示例更改为那个),要么应该将finep 移到结构的末尾。

【讨论】:

  • 这实际上使情况变得更糟。 sizeof(struct s_zoneData) 在这种情况下是 64,这意味着它在 finep 之前添加了 7 个填充字节,在 pad 之前添加了 1 个填充字节。但是,将pad 成员移动到结构中的第一位会使结构大小保持在 56 字节,实际上是指填充字节。
  • @Guille 你是对的,为了避免内部填充,还需要一些成员布局。但除此之外,这个想法仍然成立。我会更改答案以包含您的评论。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-10
  • 1970-01-01
相关资源
最近更新 更多