Skip to content

[libc++] std::cmp_less and other integer comparison functions could be improved #65136

@eisenwave

Description

@eisenwave

The current implementation of the safe comparison functions is less not optimal:

template<__is_safe_integral_cmp _Tp, __is_safe_integral_cmp _Up>
_LIBCPP_INLINE_VISIBILITY constexpr
bool cmp_less(_Tp __t, _Up __u) noexcept
{
if constexpr (is_signed_v<_Tp> == is_signed_v<_Up>)
return __t < __u;
else if constexpr (is_signed_v<_Tp>)
return __t < 0 ? true : make_unsigned_t<_Tp>(__t) < __u;
else
return __u < 0 ? false : __t < make_unsigned_t<_Up>(__u);
}

For two maximally wide integers (i.e. unsigned long), this is the best you can do. However, if a wider integer type is available, then all comparisons can be made safe by converting to the wider integer.

For example, a comparison unsigned int < signed int could be implemented through long long < long long, which is either zero-cost, or involves a sign extension for one of the operands. Indeed, if we compare std::cmp_less to such an extending implementation: (https://godbolt.org/z/M1s5dxh11)

bool std_less(unsigned int a, signed int b) { return std::cmp_less(a, b); }
bool ext_less(unsigned int a, signed int b) { return static_cast<long long>(a) < static_cast<long long>(b); }

The output for ext_less is slightly better, although it's not strikingly obvious. The additional mov and setl in ext_less are artifacts of the ABI, and movsxd could also disappear through inlining.

std_less(unsigned int, int):
        test    esi, esi
        setns   cl
        cmp     edi, esi
        setb    al
        and     al, cl
        ret
ext_less(unsigned int, int):
        mov     eax, edi
        movsxd  rcx, esi
        cmp     rax, rcx
        setl    al
        ret

When we add __builtin_assume(a <= INT_MAX); to both functions, the difference becomes more striking. std_less is completely unchanged, but ext_less becomes:

ext_less(unsigned int, int):
        cmp     edi, esi
        setl    al
        ret

This would turn std::cmp_less into a true zero-overhead utility, since LLVM could now always fall back onto a simple cmp instruction if it knows that the unsigned operand is restricted in its value range (which is quite plausible).

Proposed Change

template<__is_safe_integral_cmp _Tp, __is_safe_integral_cmp _Up>
_LIBCPP_INLINE_VISIBILITY constexpr
bool cmp_less(_Tp __t, _Up __u) noexcept
{
+  if constexpr (sizeof(_Tp) <= sizeof(long long) && sizeof(_Up) <= sizeof(long long))
+    return static_cast<long long>(__t) < static_cast<long long>(__u);
-  if constexpr (is_signed_v<_Tp> == is_signed_v<_Up>)
+  else if constexpr (is_signed_v<_Tp> == is_signed_v<_Up>)
    return __t < __u;
  else if constexpr (is_signed_v<_Tp>)
    return __t < 0 ? true : make_unsigned_t<_Tp>(__t) < __u;
  else
    return __u < 0 ? false : __t < make_unsigned_t<_Up>(__u);
}

The same style of change could be applied to the std::cmp_equal.

Metadata

Metadata

Assignees

Labels

good first issuehttps://github.com/llvm/llvm-project/contributelibc++libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.performance

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions