我有一个复合索引类型,由两个16位整数打包成一个32位对象组成,旨在传递和处理类似指针的东西。但是我注意到我定义的比较运算符没有按照我预期的方式进行优化。
给定以下简化代码:
其中一个似乎比另一个做更多的工作。但我只是看不出它们有什么不同:断言保证结构体的布局是这样的,即将其作为
用
令我感兴趣的是,根据编译器资源管理器,所有主要编译器(GCC、Clang、Intel、MSVC)似乎都做了大致相同的事情。这降低了这是一个优化器疏忽的机会,但我看不出我的评估实际上是错误的。
因此,这有两个部分:
1)
2)如果是,为什么编译器选择不发出较短的指令序列?
我确定这种类型的优化一定是编译器知道的,因为它们对于加速其他更严重的代码(如将数据压入向量寄存器以一次比较更多数据的
给定以下简化代码:
#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位比较,而不是一次性比较所有数据,这表明短路可能不是问题(尽管它确实引发了一个相关的问题,即为什么它不能在两种情况下编译&&
和&
- 肯定有一种方法在这两种情况下都更好)。令我感兴趣的是,根据编译器资源管理器,所有主要编译器(GCC、Clang、Intel、MSVC)似乎都做了大致相同的事情。这降低了这是一个优化器疏忽的机会,但我看不出我的评估实际上是错误的。
因此,这有两个部分:
1)
equal1
是否真的与equal2
做同样的事情?我错过了什么疯狂的东西吗?2)如果是,为什么编译器选择不发出较短的指令序列?
我确定这种类型的优化一定是编译器知道的,因为它们对于加速其他更严重的代码(如将数据压入向量寄存器以一次比较更多数据的
memcmp
等)会更有用。
&&
运算符的要求? - Mark Ransom&
运算符替换&&
”的内容。 - Quentinalignas (std::uint32_t)
添加到 TwoParter 的声明中使它们变得相同。想把它变成一个答案吗?我以为在 x86 上 int 没有对齐要求? - Alex Celeste