From 6f671cdb7685326f17f70c8bfa9f56583a2e1448 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 6 Jul 2026 11:49:07 +0200 Subject: [PATCH 1/6] Use native __int128 comparison in wide::integer operator_less / operator_greater The generic limb-by-limb comparison loop in wide::integer::_impl::operator_less and operator_greater compiles to data-dependent branches: an early-exit branch per limb plus a sign pre-check branch for signed types. In columnar comparison loops (NumComparisonImpl in FunctionsComparison.h) these branches prevent vectorization and mispredict heavily on real data. The scalar branchy kernel is visible in release builds: the compiled NumComparisonImpl::vectorVector processes one element per iteration with two data-dependent branches, while EqualsOp gets a proper SIMD loop. For 128-bit same-type comparisons on little-endian platforms, compare via native (unsigned) __int128 instead: std::bit_cast to the native type and a single comparison, which compiles to branchless cmp/sbb. Two's complement order of __int128 matches the existing semantics exactly (sign pre-check plus unsigned lexicographic limb comparison). Standalone kernel benchmark of the columnar less-than loop (clang 21, -march=x86-64-v3, 64k-element blocks, GB/s of processed data, higher is better): data shape before after UInt128, low 64 bits only 46.9 56.1 UInt128, fully random 55.7 56.9 UInt128, 50% equal high limb 8.4 57.0 Int128, low 64 bits only 31.2 51.8 Int128, fully random 8.0 51.8 Int128, 50% equal high limb 8.1 47.9 The worst cases (unpredictable limb-equality or sign patterns) improve up to 6.8x, and no data shape regresses. 256-bit comparisons are left unchanged: the branchy early-exit loop wins on predictable data there and the branchless alternative regresses it, so it is not a clear improvement. Result equivalence with the previous implementation was verified by an exhaustive check of all comparison operators (<, >, <=, >=, ==, !=) for UInt128/Int128/UInt256/Int256 over boundary values (0, 1, -1, min, max, sign bit, 2^64 boundaries) and 12 million structured-random pairs (equal limbs, off-by-one limbs, negated pairs, equal values), plus mixed-operand shapes and constexpr evaluation. Co-Authored-By: Claude Fable 5 --- base/base/wide_integer_impl.h | 18 ++++++++ tests/performance/wide_integer_comparison.xml | 42 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/performance/wide_integer_comparison.xml diff --git a/base/base/wide_integer_impl.h b/base/base/wide_integer_impl.h index 27297dcfb7f1..ae318b5690b0 100644 --- a/base/base/wide_integer_impl.h +++ b/base/base/wide_integer_impl.h @@ -933,6 +933,14 @@ struct integer::_impl { if constexpr (should_keep_size()) { +#ifdef __SIZEOF_INT128__ + if constexpr (Bits == 128 && std::is_same_v> && std::endian::native == std::endian::little) + { + /// See the rationale in operator_less. + using Native = std::conditional_t, __int128, unsigned __int128>; + return std::bit_cast(lhs) > std::bit_cast(rhs); + } +#endif if (std::numeric_limits::is_signed && (is_negative(lhs) != is_negative(rhs))) return is_negative(rhs); @@ -959,6 +967,16 @@ struct integer::_impl { if constexpr (should_keep_size()) { +#ifdef __SIZEOF_INT128__ + if constexpr (Bits == 128 && std::is_same_v> && std::endian::native == std::endian::little) + { + /// Native 128-bit comparison compiles to branchless code (cmp/sbb), while the generic limb loop + /// below compiles to data-dependent branches that mispredict on real data and prevent + /// vectorization of columnar comparison loops. + using Native = std::conditional_t, __int128, unsigned __int128>; + return std::bit_cast(lhs) < std::bit_cast(rhs); + } +#endif if (std::numeric_limits::is_signed && (is_negative(lhs) != is_negative(rhs))) return is_negative(lhs); diff --git a/tests/performance/wide_integer_comparison.xml b/tests/performance/wide_integer_comparison.xml new file mode 100644 index 000000000000..98c58debf54b --- /dev/null +++ b/tests/performance/wide_integer_comparison.xml @@ -0,0 +1,42 @@ + + + + CREATE TABLE cmp128 + ( + u1 UInt128, u2 UInt128, + i1 Int128, i2 Int128, + small_u1 UInt128, small_u2 UInt128, + mixed_u1 UInt128, mixed_u2 UInt128 + ) ENGINE = Memory + + + INSERT INTO cmp128 SELECT + bitShiftLeft(toUInt128(cityHash64(number, 1)), 64) + cityHash64(number, 2), + bitShiftLeft(toUInt128(cityHash64(number, 3)), 64) + cityHash64(number, 4), + toInt128(bitShiftLeft(toUInt128(cityHash64(number, 5)), 64) + cityHash64(number, 6)), + toInt128(bitShiftLeft(toUInt128(cityHash64(number, 7)), 64) + cityHash64(number, 8)), + toUInt128(cityHash64(number, 9)), + toUInt128(cityHash64(number, 10)), + bitShiftLeft(toUInt128(cityHash64(number, 11)), 64) + cityHash64(number, 12), + bitShiftLeft(toUInt128(cityHash64(number, if(number % 2 = 0, 11, 13))), 64) + cityHash64(number, 14) + FROM numbers(10000000) + + + + 1 + + + SELECT count() FROM cmp128 WHERE u1 < u2 + SELECT count() FROM cmp128 WHERE i1 < i2 + SELECT count() FROM cmp128 WHERE small_u1 < small_u2 + SELECT count() FROM cmp128 WHERE mixed_u1 < mixed_u2 + SELECT count() FROM cmp128 WHERE u1 > u2 + SELECT count() FROM cmp128 WHERE i1 >= i2 + SELECT count() FROM cmp128 WHERE u1 < bitShiftLeft(toUInt128(1), 127) + SELECT i1 FROM cmp128 ORDER BY i1 LIMIT 10 + SELECT u1 FROM cmp128 ORDER BY u1 DESC LIMIT 10 + + DROP TABLE IF EXISTS cmp128 + From c19cebd3a05d45e5fce8f50e7dbf04e091666eb1 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 6 Jul 2026 12:01:04 +0200 Subject: [PATCH 2/6] Address review nits: comment placement in wide_integer_impl.h and perf test XML Co-Authored-By: Claude Fable 5 --- base/base/wide_integer_impl.h | 8 ++++---- tests/performance/wide_integer_comparison.xml | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/base/base/wide_integer_impl.h b/base/base/wide_integer_impl.h index ae318b5690b0..dfadb06e7a79 100644 --- a/base/base/wide_integer_impl.h +++ b/base/base/wide_integer_impl.h @@ -936,7 +936,9 @@ struct integer::_impl #ifdef __SIZEOF_INT128__ if constexpr (Bits == 128 && std::is_same_v> && std::endian::native == std::endian::little) { - /// See the rationale in operator_less. + /// Native 128-bit comparison compiles to branchless code (cmp/sbb), while the generic limb loop + /// below compiles to data-dependent branches that mispredict on real data and prevent + /// vectorization of columnar comparison loops. using Native = std::conditional_t, __int128, unsigned __int128>; return std::bit_cast(lhs) > std::bit_cast(rhs); } @@ -970,9 +972,7 @@ struct integer::_impl #ifdef __SIZEOF_INT128__ if constexpr (Bits == 128 && std::is_same_v> && std::endian::native == std::endian::little) { - /// Native 128-bit comparison compiles to branchless code (cmp/sbb), while the generic limb loop - /// below compiles to data-dependent branches that mispredict on real data and prevent - /// vectorization of columnar comparison loops. + /// See the rationale in operator_greater. using Native = std::conditional_t, __int128, unsigned __int128>; return std::bit_cast(lhs) < std::bit_cast(rhs); } diff --git a/tests/performance/wide_integer_comparison.xml b/tests/performance/wide_integer_comparison.xml index 98c58debf54b..94aa4aa9fe54 100644 --- a/tests/performance/wide_integer_comparison.xml +++ b/tests/performance/wide_integer_comparison.xml @@ -1,7 +1,8 @@ - + + CREATE TABLE cmp128 ( From 9cb4bb033832acbace31086a90964d98b657d956 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 6 Jul 2026 13:49:55 +0200 Subject: [PATCH 3/6] Add correctness test for 128-bit comparison fast path The review asked for a focused regression test for the new native __int128 fast path in wide::integer::_impl::operator_less and operator_greater. Add a gtest that checks all six comparison operators for Int128 and UInt128 over boundary values (min/max, sign-bit transitions, 2^64 limb boundaries, equal-high-limb pairs) against the 256-bit comparison as an independent oracle: 256-bit comparisons use the generic limb-wise path that the fast path replaced for 128 bits. Also check constexpr evaluation of the fast path via static_assert and a few hand-picked direct assertions that do not depend on the oracle. Co-Authored-By: Claude Fable 5 --- src/Common/tests/gtest_wide_integer.cpp | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/Common/tests/gtest_wide_integer.cpp b/src/Common/tests/gtest_wide_integer.cpp index dbf2629bcf29..8f112899fcbf 100644 --- a/src/Common/tests/gtest_wide_integer.cpp +++ b/src/Common/tests/gtest_wide_integer.cpp @@ -180,6 +180,87 @@ GTEST_TEST(WideInteger, Arithmetic) } +/// The 128-bit same-type ordering operators take a fast path via native __int128 +/// (see operator_less / operator_greater in wide_integer_impl.h), while 256-bit +/// comparisons use the generic limb-wise loop. Comparing each pair in both widths +/// checks the fast path against an independent implementation. +template +static void checkComparisonAgainstWiderOracle(const std::vector & values) +{ + for (const T128 & lhs : values) + { + for (const T128 & rhs : values) + { + const T256 wide_lhs = lhs; + const T256 wide_rhs = rhs; + + EXPECT_EQ(lhs < rhs, wide_lhs < wide_rhs) << toString(lhs) << " < " << toString(rhs); + EXPECT_EQ(lhs > rhs, wide_lhs > wide_rhs) << toString(lhs) << " > " << toString(rhs); + EXPECT_EQ(lhs <= rhs, wide_lhs <= wide_rhs) << toString(lhs) << " <= " << toString(rhs); + EXPECT_EQ(lhs >= rhs, wide_lhs >= wide_rhs) << toString(lhs) << " >= " << toString(rhs); + EXPECT_EQ(lhs == rhs, wide_lhs == wide_rhs) << toString(lhs) << " == " << toString(rhs); + EXPECT_EQ(lhs != rhs, wide_lhs != wide_rhs) << toString(lhs) << " != " << toString(rhs); + } + } +} + + +GTEST_TEST(WideInteger, Comparison128Boundaries) +{ + /// Constexpr evaluation must take the same fast path. + static_assert(std::numeric_limits::min() < Int128(-1)); + static_assert(Int128(-1) < Int128(0)); + static_assert(Int128(0) < std::numeric_limits::max()); + static_assert(!(Int128(-1) < Int128(-1))); + static_assert(UInt128(0) < std::numeric_limits::max()); + static_assert((UInt128(1) << 127) > ((UInt128(1) << 127) - 1)); + + { + const Int128 min = std::numeric_limits::min(); + const Int128 max = std::numeric_limits::max(); + const Int128 two_pow_64 = Int128(1) << 64; + const Int128 high_limb = Int128(5) << 64; + + ASSERT_LT(min, Int128(-1)); + ASSERT_LT(Int128(-1), Int128(0)); + ASSERT_LT(Int128(0), Int128(1)); + ASSERT_LT(Int128(1), max); + ASSERT_LT(min, max); + ASSERT_LT(-two_pow_64, Int128(-1)); + ASSERT_LT(high_limb, high_limb + 1); + + checkComparisonAgainstWiderOracle({ + 0, 1, -1, 2, -2, + min, min + 1, max, max - 1, + two_pow_64 - 1, two_pow_64, two_pow_64 + 1, + -(two_pow_64 - 1), -two_pow_64, -(two_pow_64 + 1), + high_limb - 1, high_limb, high_limb + 1, + -(high_limb - 1), -high_limb, -(high_limb + 1), + }); + } + + { + const UInt128 max = std::numeric_limits::max(); + const UInt128 sign_bit = UInt128(1) << 127; + const UInt128 two_pow_64 = UInt128(1) << 64; + const UInt128 high_limb = UInt128(5) << 64; + + ASSERT_LT(UInt128(0), UInt128(1)); + ASSERT_LT(sign_bit - 1, sign_bit); + ASSERT_LT(sign_bit, max); + ASSERT_LT(high_limb, high_limb + 1); + + checkComparisonAgainstWiderOracle({ + 0, 1, 2, + max, max - 1, + two_pow_64 - 1, two_pow_64, two_pow_64 + 1, + sign_bit - 1, sign_bit, sign_bit + 1, + high_limb - 1, high_limb, high_limb + 1, + }); + } +} + + GTEST_TEST(WideInteger, DecimalArithmetic) { Decimal128 zero{}; From 7e5b16ae755cb921d532db1110197d154d958364 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 6 Jul 2026 20:39:58 +0200 Subject: [PATCH 4/6] Replace the native __int128 fast path with a generic branchless limb comparison The bit_cast to __int128 fixed the x86-64-v3 baseline (branchless cmp/sbb instead of a mispredicting per-limb loop) but de-vectorized the x86-64-v4 clones of the columnar comparison kernels: clang cannot re-lift a 128-bit scalar compare into AVX-512 mask compares, so the v4 vectorVector kernel extracted every limb back to GPRs (vpextrq + sbb) instead of comparing 8 values per iteration with vpcmpltuq. This matched the CI perf report: i1 < i2 +15% and i1 >= i2 +17% on x86, while ARM and ORDER BY sped up. Instead, make operator_less and operator_greater branchless for all widths, as suggested in review: compare the most significant limb as signed for signed types (subsuming the sign check), then fold in the lower limbs with carry-free boolean logic and no early-out. This autovectorizes everywhere: the v3 kernels now use AVX2 vpcmpgtq, the v4 kernels keep the same AVX-512 vpcmpltuq/vpcmpgtq/vpcmpeqq shape as master, and the 256-bit kernels become branchless too, instead of being limited to the 128-bit case. Co-Authored-By: Claude Fable 5 --- base/base/wide_integer_impl.h | 68 +++++++++++++------------ src/Common/tests/gtest_wide_integer.cpp | 7 ++- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/base/base/wide_integer_impl.h b/base/base/wide_integer_impl.h index dfadb06e7a79..51def63fa0bc 100644 --- a/base/base/wide_integer_impl.h +++ b/base/base/wide_integer_impl.h @@ -933,29 +933,31 @@ struct integer::_impl { if constexpr (should_keep_size()) { -#ifdef __SIZEOF_INT128__ - if constexpr (Bits == 128 && std::is_same_v> && std::endian::native == std::endian::little) - { - /// Native 128-bit comparison compiles to branchless code (cmp/sbb), while the generic limb loop - /// below compiles to data-dependent branches that mispredict on real data and prevent - /// vectorization of columnar comparison loops. - using Native = std::conditional_t, __int128, unsigned __int128>; - return std::bit_cast(lhs) > std::bit_cast(rhs); - } -#endif - if (std::numeric_limits::is_signed && (is_negative(lhs) != is_negative(rhs))) - return is_negative(rhs); - integer t = rhs; - for (unsigned i = 0; i < item_count; ++i) + + /// Branchless lexicographic comparison from the most significant limb: no early-out, so the + /// columnar comparison kernels stay branchless and vectorize (e.g. to AVX-512 vpcmpq). The top + /// limb is compared as signed for signed types, which subsumes the sign check; lower limbs unsigned. + base_type lhs_top = lhs.items[big(0)]; + base_type rhs_top = get_item(t, big(0)); + + bool greater; + if constexpr (std::is_same_v) + greater = static_cast(lhs_top) > static_cast(rhs_top); + else + greater = lhs_top > rhs_top; + + bool equal = lhs_top == rhs_top; + for (unsigned i = 1; i < item_count; ++i) { + base_type lhs_item = lhs.items[big(i)]; base_type rhs_item = get_item(t, big(i)); - if (lhs.items[big(i)] != rhs_item) - return lhs.items[big(i)] > rhs_item; + greater = greater | (equal & (lhs_item > rhs_item)); + equal = equal & (lhs_item == rhs_item); } - return false; + return greater; } else { @@ -969,27 +971,29 @@ struct integer::_impl { if constexpr (should_keep_size()) { -#ifdef __SIZEOF_INT128__ - if constexpr (Bits == 128 && std::is_same_v> && std::endian::native == std::endian::little) - { - /// See the rationale in operator_greater. - using Native = std::conditional_t, __int128, unsigned __int128>; - return std::bit_cast(lhs) < std::bit_cast(rhs); - } -#endif - if (std::numeric_limits::is_signed && (is_negative(lhs) != is_negative(rhs))) - return is_negative(lhs); - integer t = rhs; - for (unsigned i = 0; i < item_count; ++i) + + /// See the rationale in operator_greater. + base_type lhs_top = lhs.items[big(0)]; + base_type rhs_top = get_item(t, big(0)); + + bool less; + if constexpr (std::is_same_v) + less = static_cast(lhs_top) < static_cast(rhs_top); + else + less = lhs_top < rhs_top; + + bool equal = lhs_top == rhs_top; + for (unsigned i = 1; i < item_count; ++i) { + base_type lhs_item = lhs.items[big(i)]; base_type rhs_item = get_item(t, big(i)); - if (lhs.items[big(i)] != rhs_item) - return lhs.items[big(i)] < rhs_item; + less = less | (equal & (lhs_item < rhs_item)); + equal = equal & (lhs_item == rhs_item); } - return false; + return less; } else { diff --git a/src/Common/tests/gtest_wide_integer.cpp b/src/Common/tests/gtest_wide_integer.cpp index 8f112899fcbf..edc149849b2d 100644 --- a/src/Common/tests/gtest_wide_integer.cpp +++ b/src/Common/tests/gtest_wide_integer.cpp @@ -180,10 +180,9 @@ GTEST_TEST(WideInteger, Arithmetic) } -/// The 128-bit same-type ordering operators take a fast path via native __int128 -/// (see operator_less / operator_greater in wide_integer_impl.h), while 256-bit -/// comparisons use the generic limb-wise loop. Comparing each pair in both widths -/// checks the fast path against an independent implementation. +/// The ordering operators use a branchless limb-wise comparison +/// (see operator_less / operator_greater in wide_integer_impl.h). +/// Comparing each pair in both widths cross-checks the 2-limb and 4-limb instantiations. template static void checkComparisonAgainstWiderOracle(const std::vector & values) { From 79a6d8b103c895fc3d183a0c500c9c1c81ed4034 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 6 Jul 2026 12:10:25 +0200 Subject: [PATCH 5/6] Branchless sign extension in wide::integer conversions wide_integer_from_builtin and wide_integer_from_wide_integer sign-extend through an `if (rhs < 0)` branch that selects between two separate fill loops. In columnar conversion loops (ConvertImpl) this data-dependent branch mispredicts on mixed-sign data and prevents vectorization even when it predicts well. Select the extension value (0 or -1) branchlessly instead and use a single fill loop. Standalone kernel benchmark of the columnar conversion loop (clang 21, -march=x86-64-v3, 64k-element blocks, GB/s of processed data): conversion before after Int64 -> Int256, random sign 10.3 73.9 Int64 -> Int256, positive only 31.3 73.3 Int128 -> Int256, random sign 12.2 68.0 Int64 -> Int128 61.5 62.2 UInt64 -> UInt128/UInt256 unchanged The same effect is visible at the query level: with the official 26.6.1 binary, `toInt256(toInt64(cityHash64(number)))` over 100M rows takes 6.8x the time of the equivalent unsigned conversion in a single thread, and 3.5x the time of the same conversion with positive-only values. Result equivalence with the previous implementation was verified over 36 million checks: all builtin integral types (int8/16/32/64, unsigned variants) converted to UInt128/Int128/UInt256/Int256 including type boundary values, 128-to-256-bit widening for all signedness combinations, narrowing, and constexpr evaluation. Co-Authored-By: Claude Fable 5 --- base/base/wide_integer_impl.h | 27 +++++++------------ tests/performance/wide_integer_conversion.xml | 15 +++++++++++ 2 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 tests/performance/wide_integer_conversion.xml diff --git a/base/base/wide_integer_impl.h b/base/base/wide_integer_impl.h index 51def63fa0bc..f06c77d3a197 100644 --- a/base/base/wide_integer_impl.h +++ b/base/base/wide_integer_impl.h @@ -372,18 +372,14 @@ struct integer::_impl self.items[little(0)] = _impl::to_Integral(rhs); + /// Sign-extend with a value select instead of a branch: a data-dependent branch here + /// mispredicts on mixed-sign data and prevents vectorization of columnar conversion loops. + base_type extension = 0; if constexpr (std::is_signed_v) - { - if (rhs < 0) - { - for (unsigned i = 1; i < item_count; ++i) - self.items[little(i)] = -1; - return; - } - } + extension = rhs < 0 ? base_type(-1) : base_type(0); for (unsigned i = 1; i < item_count; ++i) - self.items[little(i)] = 0; + self.items[little(i)] = extension; } template @@ -531,18 +527,13 @@ struct integer::_impl if constexpr (Bits > Bits2) { + /// See the rationale in wide_integer_from_builtin. + base_type extension = 0; if constexpr (std::is_signed_v) - { - if (rhs < 0) - { - for (unsigned i = to_copy; i < item_count; ++i) - self.items[little(i)] = -1; - return; - } - } + extension = rhs < 0 ? base_type(-1) : base_type(0); for (unsigned i = to_copy; i < item_count; ++i) - self.items[little(i)] = 0; + self.items[little(i)] = extension; } } diff --git a/tests/performance/wide_integer_conversion.xml b/tests/performance/wide_integer_conversion.xml new file mode 100644 index 000000000000..5af6cb00f42f --- /dev/null +++ b/tests/performance/wide_integer_conversion.xml @@ -0,0 +1,15 @@ + + + + + 1 + + + SELECT count() FROM numbers(50000000) WHERE NOT ignore(toInt256(toInt64(cityHash64(number)))) + SELECT count() FROM numbers(50000000) WHERE NOT ignore(toInt256(toInt64(bitAnd(cityHash64(number), 4611686018427387903)))) + SELECT count() FROM numbers(50000000) WHERE NOT ignore(toUInt256(cityHash64(number))) + SELECT count() FROM numbers(50000000) WHERE NOT ignore(toInt256(toInt128(toInt64(cityHash64(number))))) + SELECT count() FROM numbers(50000000) WHERE NOT ignore(toInt128(toInt64(cityHash64(number)))) + From 8d36d9363742cb356ff771b0c5ff6a63b0599621 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 6 Jul 2026 23:54:10 +0200 Subject: [PATCH 6/6] Cover 256-bit comparisons whose decisive limb is above bit 127 The existing checkComparisonAgainstWiderOracle only widens Int128/UInt128 inputs, so every Int256/UInt256 operand had bits 255:128 fixed to sign extension or zero and the upper two limbs of the 4-limb walk in operator_less/operator_greater were never exercised. Add a strictly-ascending oracle (comparisons must agree with the index order, independent of the comparison implementation) and a Comparison256Boundaries test with native Int256/UInt256 pairs whose first differing limb is each of the upper 64-bit chunks: around 1 << 128 and 1 << 192, the unsigned top bit, and signed negatives that share a top limb. Co-Authored-By: Claude Opus 4.8 --- src/Common/tests/gtest_wide_integer.cpp | 85 +++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/Common/tests/gtest_wide_integer.cpp b/src/Common/tests/gtest_wide_integer.cpp index edc149849b2d..1d068361dfc1 100644 --- a/src/Common/tests/gtest_wide_integer.cpp +++ b/src/Common/tests/gtest_wide_integer.cpp @@ -204,6 +204,31 @@ static void checkComparisonAgainstWiderOracle(const std::vector & values) } +/// Oracle for the full 4-limb walk: the widen-to-256 helper above keeps bits 255:128 fixed to +/// sign extension or zero, so it never exercises a decisive difference in the upper two limbs. +/// Here the caller supplies a strictly ascending sequence and every pair is checked against the +/// index order, which is independent of the comparison implementation under test. +template +static void checkStrictlyAscending(const std::vector & values) +{ + for (size_t i = 0; i < values.size(); ++i) + { + for (size_t j = 0; j < values.size(); ++j) + { + const T & lhs = values[i]; + const T & rhs = values[j]; + + EXPECT_EQ(lhs < rhs, i < j) << toString(lhs) << " < " << toString(rhs); + EXPECT_EQ(lhs > rhs, i > j) << toString(lhs) << " > " << toString(rhs); + EXPECT_EQ(lhs <= rhs, i <= j) << toString(lhs) << " <= " << toString(rhs); + EXPECT_EQ(lhs >= rhs, i >= j) << toString(lhs) << " >= " << toString(rhs); + EXPECT_EQ(lhs == rhs, i == j) << toString(lhs) << " == " << toString(rhs); + EXPECT_EQ(lhs != rhs, i != j) << toString(lhs) << " != " << toString(rhs); + } + } +} + + GTEST_TEST(WideInteger, Comparison128Boundaries) { /// Constexpr evaluation must take the same fast path. @@ -260,6 +285,66 @@ GTEST_TEST(WideInteger, Comparison128Boundaries) } +GTEST_TEST(WideInteger, Comparison256Boundaries) +{ + /// Constexpr evaluation must take the same fast path, with the decisive difference above bit 127. + static_assert((Int256(1) << 192) > (Int256(1) << 128)); + static_assert((Int256(1) << 128) > Int256(0)); + static_assert(std::numeric_limits::min() < (Int256(1) << 128)); + static_assert((UInt256(1) << 255) > (UInt256(1) << 192)); + + { + const Int256 min = std::numeric_limits::min(); + const Int256 max = std::numeric_limits::max(); + const Int256 limb2 = Int256(1) << 128; + const Int256 limb3 = Int256(1) << 192; + + checkStrictlyAscending({ + min, + min + 1, + -limb3, + -limb3 + 1, + -limb2 - 1, + -limb2, + -limb2 + 1, + -1, + 0, + 1, + limb2 - 1, + limb2, + limb2 + 1, + limb3 - 1, + limb3, + limb3 + 1, + max - 1, + max, + }); + } + + { + const UInt256 max = std::numeric_limits::max(); + const UInt256 limb2 = UInt256(1) << 128; + const UInt256 limb3 = UInt256(1) << 192; + const UInt256 top_bit = UInt256(1) << 255; + + checkStrictlyAscending({ + 0, + 1, + limb2 - 1, + limb2, + limb2 + 1, + limb3 - 1, + limb3, + limb3 + 1, + top_bit, + top_bit + 1, + max - 1, + max, + }); + } +} + + GTEST_TEST(WideInteger, DecimalArithmetic) { Decimal128 zero{};