【发布时间】: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,所以编译器已经知道会是这样。 && 的短路行为对输出应该不重要,因为它的右手操作数没有效果(编译器可以看到这一点),因为没有其他有趣的事情发生,我无法想象为什么它会想要例如推迟加载数据的后半部分。
将&& 替换为& 运算符可以消除跳转,但不会从根本上改变代码的作用:它仍然会生成两个单独的16 位比较,而不是一次比较所有数据,这表明短路可能不是问题(尽管它确实提出了一个相关的问题,即为什么它不能以相同的方式编译 && 和 & - 当然两者之一应该在两种情况)。
让我感兴趣的是,根据 Compiler Explorer 的说法,所有主要编译器(GCC、Clang、Intel、MSVC)似乎都在做大致相同的事情。这减少了这是某个优化器疏忽的可能性,但我看不出我自己对此的评估实际上是错误的。
所以这有两个部分:
1) equal1 真的和equal2 做同样的事情吗?我在这里错过了什么疯狂的东西吗?
2) 如果是,为什么编译器会选择不发出较短的指令序列?
我确信这种优化一定是编译器知道的,因为它们对于加速其他更严重的代码(例如memcmp 将东西塞入向量寄存器以一次比较更多数据。
【问题讨论】:
-
可能是因为需要短路
&&操作符? -
@MarkRansom 这可能就是“用
&运算符替换&&”的意义所在。 -
带有
&&运算符的版本可以通过引入可能导致竞争条件的读取来轻松解释,但我对&毫无头绪。 -
@Jarod42 字节序?这不应该妨碍按位比较,不是吗?
-
@Jarod42 很好,将
alignas (std::uint32_t)添加到 TwoParter 的声明中使它们相同。想把它变成答案吗?我认为 ints 在 x86 上没有对齐要求?
标签: c++ assembly optimization