我有一个复合索引类型,它由两个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
的比较必须比较所有相同的数据而不是检查uint16_t
字段分开。更重要的是,这是x86,因此编译器已经知道情况就是如此。 &&
的短路行为对输出不应该是重要的,因为它的右手操作数没有效果(并且编译器可以看到这个),并且因为没有其他有趣的事情发生我无法想象为什么它会想要例如推迟加载数据的后半部分。
用&&
运算符替换&
可以摆脱跳转,但不会从根本上改变代码的作用:它仍会生成两个独立的16位比较,而不是比较所有数据一次性,这表明短路可能不是问题(尽管它确实提出了一个相关的问题,为什么它不能以同样的方式编译&&
和&
- 肯定是其中之一在这两种情况下,两者应该“更好”。)
我感兴趣的是,根据Compiler Explorer,所有主要的编译器(GCC,Clang,Intel,MSVC)似乎都做了大致相同的事情。这降低了这是一个优化器监督的可能性,但我看不出的是我自己对此的评估实际上是错误的。
所以这有两个部分:
1)equal1
真的和equal2
做同样的事吗?我在这里错过了一些疯狂的东西吗?
2)如果确实如此,为什么编译器会选择不发出较短的指令序列?
我确信这种情况下的优化 必须是编译器所知道的,因为它们对于加速其他更严重的代码(例如, memcmp
将事物推入向量寄存器,以便一次比较更多数据。
答案 0 :(得分:11)
对齐要求不一样,TwoParter
的对齐方式与std::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