【问题标题】:Why doesn't this short comparison optimize the way I expected?为什么这个简短的比较没有优化我预期的方式?
【发布时间】:2017-05-11 22:15:56
【问题描述】:

我有一个复合索引类型,它由两个 16 位整数打包在一起组成一个 32 位对象,该对象旨在传递并有点像指针一样处理。但我偶然注意到我定义的比较运算符并没有按照我预期的方式进行优化。

鉴于此精简代码:

#include <cstdint>

struct TwoParter {
    std::uint16_t blk;
    std::uint16_t ofs;
};
static_assert (sizeof(TwoParter) == sizeof(std::uint32_t), "pack densely");

bool equal1 (TwoParter const & lhs, TwoParter const & rhs) {
    return lhs.blk == rhs.blk && lhs.ofs == rhs.ofs;
}

bool equal2 (TwoParter const & lhs, TwoParter const & rhs) {
    auto lp = reinterpret_cast <std::uint32_t const *> (&lhs);
    auto rp = reinterpret_cast <std::uint32_t const *> (&rhs);
    return *lp == *rp;
}

GCC(Compiler Explorer 上的 7.1)生成以下程序集(选项 -m64 -std=c++11 -O3):

equal1(TwoParter const&, TwoParter const&):
        movzwl  (%rsi), %edx
        xorl    %eax, %eax
        cmpw    %dx, (%rdi)
        je      .L5
        rep ret
.L5:
        movzwl  2(%rsi), %eax
        cmpw    %ax, 2(%rdi)
        sete    %al
        ret
equal2(TwoParter const&, TwoParter const&):
        movl    (%rsi), %eax
        cmpl    %eax, (%rdi)
        sete    %al
        ret

其中一个似乎比另一个做得更多。但我只是看不出它们有什么不同:断言保证结构的布局使得作为uint23_t的比较必须比较所有相同的数据作为检查@ 987654326@ 字段分开。更重要的是,这是 x86,所以编译器已经知道会是这样。 &amp;&amp; 的短路行为对输出应该不重要,因为它的右手操作数没有效果(编译器可以看到这一点),因为没有其他有趣的事情发生,我无法想象为什么它会想要例如推迟加载数据的后半部分。

&amp;&amp; 替换为&amp; 运算符可以消除跳转,但不会从根本上改变代码的作用:它仍然会生成两个单独的16 位比较,而不是一次比较所有数据,这表明短路可能不是问题(尽管它确实提出了一个相关的问题,即为什么它不能以相同的方式编译 &amp;&amp;&amp; - 当然两者之一应该在两种情况)。

让我感兴趣的是,根据 Compiler Explorer 的说法,所有主要编译器(GCC、Clang、Intel、MSVC)似乎都在做大致相同的事情。这减少了这是某个优化器疏忽的可能性,但我看不出我自己对此的评估实际上是错误的。

所以这有两个部分:

1) equal1 真的和equal2 做同样的事情吗?我在这里错过了什么疯狂的东西吗?

2) 如果是,为什么编译器会选择不发出较短的指令序列?

我确信这种优化一定是编译器知道的,因为它们对于加速其他更严重的代码(例如memcmp 将东西塞入向量寄存器以一次比较更多数据。

【问题讨论】:

  • 可能是因为需要短路&amp;&amp;操作符?
  • @MarkRansom 这可能就是“用&amp; 运算符替换&amp;&amp;”的意义所在。
  • 带有&amp;&amp; 运算符的版本可以通过引入可能导致竞争条件的读取来轻松解释,但我对&amp; 毫无头绪。
  • @Jarod42 字节序?这不应该妨碍按位比较,不是吗?
  • @Jarod42 很好,将alignas (std::uint32_t) 添加到 TwoParter 的声明中使它们相同。想把它变成答案吗?我认为 ints 在 x86 上没有对齐要求?

标签: c++ assembly optimization


【解决方案1】:

对齐要求不一样,TwoParterstd::uint16_t 对齐。

TwoParter 更改为

struct alignas(std::uint32_t) TwoParter {
    std::uint16_t blk;
    std::uint16_t ofs;
};

为 gcc 7.1 生成相同的 asm:

equal1(TwoParter const&, TwoParter const&):
        movl    (%rsi), %eax
        cmpl    %eax, (%rdi)
        sete    %al
        ret
equal2(TwoParter const&, TwoParter const&):
        movl    (%rsi), %eax
        cmpl    %eax, (%rdi)
        sete    %al
        ret

Demo

【讨论】:

  • 为什么对齐(这在 x86 上并不真正存在)会吓到编译器?
  • @harold,即使在(某些)x86 处理器上,错位不会导致潜在的减速吗?
  • 虽然我的理解是原始代码应该在 x86 上同样适用,但最好还是防御一下。也许不值得优化器花时间考虑可能较慢的访问是否仍然比单独的、正确的访问更好?
  • @zch 好吧,如果你不小心越过缓存线边界,在 Core2 上是的.. 但那是古老的历史
  • @zch 即使会,它通常在 +1 时钟左右(除非你有真正的缓存未命中),所以我仍然希望两部分 16b 代码会慢很多,即使是未对齐的读。也许优化器没有很好地覆盖它,因为任何如此关注这种情况的性能的人都应该保护对齐?
猜你喜欢
  • 2020-09-23
  • 2019-06-14
  • 2013-11-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-11
相关资源
最近更新 更多