在使用
-O3
的 GCC 13.2 和 Clang 18.0.1 中,由四个 operator <
对象组成的元组的 std::uint16_t
生成的程序集并不令人印象深刻(双关语)。使用 reinterpret_cast
和类型双关的另外两种实现都可以产生优化的装配。
编辑:由于这个问题已经被否决,我想指出默认程序集使用 3 个条件,当此代码在高性能循环中运行时,这非常有影响力。其他两种实现都有零分支。
问题1:这些实现是否正确?
问题2:如果不是,需要什么条件才能保证正确性?
问题3:假设有一个可以在编译时评估正确性的测试,为什么编译器不简单地输出优化后的程序集?
#include <cstdint>
#include <tuple>
#include <bit>
using TupleU16 = std::tuple<std::uint16_t, std::uint16_t, std::uint16_t, std::uint16_t>;
bool less(TupleU16 lhs, TupleU16 rhs) {
return lhs < rhs;
}
constexpr bool canSafelyUseTypePunning() {
TupleU16 t;
std::uint16_t* val0 = &std::get<0>(t);
std::uint16_t* val1 = &std::get<1>(t);
std::uint16_t* val2 = &std::get<2>(t);
std::uint16_t* val3 = &std::get<3>(t);
if constexpr (std::endian::native == std::endian::little) {
return sizeof(TupleU16) == 8 &&
(val0 - val3) == 3 && (val0 - val2) == 2 && (val0 - val1) == 1;
}
else if constexpr (std::endian::native == std::endian::big) {
return sizeof(TupleU16) == 8 &&
(val3 - val0) == 3 && (val2 - val0) == 2 && (val1 - val0) == 1;
}
else {
return false;
}
}
bool less2(TupleU16 lhs, TupleU16 rhs) { // cannot be constexpr
static_assert(canSafelyUseTypePunning());
auto* lhsp = reinterpret_cast<const std::uint64_t*>(&lhs);
auto* rhsp = reinterpret_cast<const std::uint64_t*>(&rhs);
return *lhsp < *rhsp;
}
bool less3(TupleU16 lhs, TupleU16 rhs) { // can be constexpr
static_assert(canSafelyUseTypePunning());
union U {
std::uint64_t u64;
TupleU16 tu16;
};
return U{.tu16 = lhs}.u64 < U{.tu16 = rhs}.u64;
}
不,那是
std::tuple
对象。std::tuple
的内存布局
std::tuple
的 operator<
对于您的用例而言不是最佳的原因可能是它保证仅在前一个元素的比较尚未得出结论时才访问字典顺序的下一个元素。这意味着它在另一个线程修改元组的其他元素的情况下提供了额外的保证。
您似乎不需要这种保证,并且更喜欢性能更高的实现。如果您需要,请自己实现与这些要求等效的
std::tuple
,并使用 std::bit_cast<uint64_t>
来获得定义的行为进行比较。另外,如果元组的所有元素类型都相等,那么 std::array
就足够了(但它具有相同的 operator<
性能问题)。