Files
ClickHouse/tests/performance/wide_integer_division.xml
Groene AI d631ef2258 Divide 128-bit integers with one 64-bit division when both fit
The Bits == 128 branch of wide::integer's divide converts both operands to
unsigned __int128 and divides unconditionally. On any target without a
double-word divide instruction that becomes a __udivti3 call, and compiler-rt's
128 / 64 kernel tests only the divisor for a zero high word, never the
numerator, so dividing two values that each fit in a single 64-bit limb still
runs Knuth's algorithm over 32-bit digits with two quotient-correction loops.
Every Decimal128 value below 2^64 pays this once per row, because rounding and
rescaling divide by the scale multiplier.

The generic code below that branch already applies this optimisation: a narrow
shortcut added in July 2026, four years after the Bits == 128 branch, which
returns before reaching it. So 128 bits was the only width excluded from a
shortcut written for exactly this case.

Test both high words in a single comparison and take a plain 64-bit division
when they are both zero, keeping the __int128 path as the fallback. The
divide-by-zero guard already precedes the branch, so the narrow path cannot
divide by zero.

Both high words are tested in one OR-compare rather than two separate
comparisons because the wide path must not pay for the guard. Measured on
x86-64 natively over 31 interleaved rounds with three byte-identical master
binaries as the noise control (spread 1.0019): one OR-compare lands at 0.9967
of master, two separate compares at 1.0256, and removing the branch entirely
at 1.0237.

Decimal128 trunc(x, -2) over 1M rows: 2.62 -> 1.71 ns/row on x86-64 (1.53x)
and 31.5 -> 13.7 ns/row on AArch64 under qemu-user (2.30x). This helps x86-64
as well as AArch64, which the report's AArch64-only measurements do not show.

Results are bit-identical: 4,514,520 operand pairs compared against an
unsigned __int128 oracle on both quotient and remainder, 0 mismatches, and the
new stateless test's reference was captured from an unpatched build.

The existing wide-integer division tests are extended rather than replaced: the
edge-case sweep was UInt256-only, so the 128-bit branch was walked only by the
random test.

Related: https://github.com/ClickHouse/ClickHouse/issues/116081
2026-08-24 19:22:41 +00:00

104 lines
5.4 KiB
XML

<test>
<!-- Division of 256-bit integers. The cost of the algorithm depends on how many 64-bit limbs
the divisor spans and on how much smaller the divisor is than the dividend, so the divisors
here are chosen to span one, two, three and four limbs, with the small-divisor case
(which is by far the most common in practice) covered separately. -->
<create_query>
CREATE TABLE div256
(
u UInt256,
i Int256,
small_u UInt256,
d40 Decimal256(40)
) ENGINE = Memory
</create_query>
<create_query>
CREATE TABLE div128
(
u UInt128,
i Int128,
small_u UInt128,
small_i Int128
) ENGINE = Memory
</create_query>
<fill_query>
INSERT INTO div256 SELECT
bitShiftLeft(toUInt256(cityHash64(number, 1)), 192) + bitShiftLeft(toUInt256(cityHash64(number, 2)), 128)
+ bitShiftLeft(toUInt256(cityHash64(number, 3)), 64) + cityHash64(number, 4),
toInt256(bitShiftLeft(toUInt256(cityHash64(number, 5)), 191) + bitShiftLeft(toUInt256(cityHash64(number, 6)), 128)
+ bitShiftLeft(toUInt256(cityHash64(number, 7)), 64) + cityHash64(number, 8)),
toUInt256(cityHash64(number, 9)),
toDecimal256(number, 40) / 7
FROM numbers(2000000)
</fill_query>
<fill_query>
INSERT INTO div128 SELECT
bitShiftLeft(toUInt128(cityHash64(number, 1)), 64) + cityHash64(number, 2),
toInt128(bitShiftLeft(toUInt128(cityHash64(number, 3)), 63) + cityHash64(number, 4)),
toUInt128(cityHash64(number, 5)),
toInt128(intDiv(cityHash64(number, 6), 2))
FROM numbers(2000000)
</fill_query>
<settings>
<max_threads>1</max_threads>
</settings>
<!-- Divisor of one limb: 10^18 and a small constant. -->
<query>SELECT sum(intDiv(u, 1000000000000000000)) FROM div256</query>
<query>SELECT sum(modulo(u, 1000000000000000000)) FROM div256</query>
<query>SELECT sum(intDiv(u, 997)) FROM div256</query>
<query>SELECT sum(intDiv(i, 1000000000000000000)) FROM div256</query>
<!-- Divisors of two, three and four limbs: 10^20, 10^40 and 10^76. -->
<query>SELECT sum(intDiv(u, toUInt256('100000000000000000000'))) FROM div256</query>
<query>SELECT sum(intDiv(u, toUInt256('10000000000000000000000000000000000000000'))) FROM div256</query>
<query>SELECT sum(intDiv(u, toUInt256('10000000000000000000000000000000000000000000000000000000000000000000000000000'))) FROM div256</query>
<!-- Both operands are full width, so the quotient is small. -->
<query>SELECT sum(intDiv(u, i + 1)) FROM div256</query>
<!-- Values that fit in a single limb but are stored in a 256-bit type. -->
<query>SELECT sum(intDiv(small_u, 997)) FROM div256</query>
<query>SELECT sum(intDiv(u, small_u + 1)) FROM div256</query>
<!-- Decimal256 formatting divides by the scale multiplier, which is 10^40 here. It is several
hundred times more expensive per row than the queries above, so these two run over a small
part of the table: over the whole of it they take longer than the 15 seconds a single query
is allowed, which fails the whole test. -->
<query>SELECT sum(length(toString(d40))) FROM (SELECT d40 FROM div256 LIMIT 200000)</query>
<query>SELECT sum(length(toDecimalString(d40, 40))) FROM (SELECT d40 FROM div256 LIMIT 200000)</query>
<!-- Rounding and rescaling a Decimal256 both divide by the scale multiplier once per row. -->
<query>SELECT sum(round(d40, 5)) FROM div256</query>
<query>SELECT sum(toDecimal256(d40, 5)) FROM div256</query>
<!-- Printing a 256-bit integer strips 18-digit blocks off with a division by 10^18 each. -->
<query>SELECT sum(length(toString(u))) FROM div256</query>
<query>SELECT sum(length(toString(i))) FROM div256</query>
<!-- `divideDecimal` and `multiplyDecimal` convert their operands to decimal digits one at a
time, which is a division by 10 per digit, so they are as expensive per row as formatting
and read a small part of the table for the same reason. -->
<query>SELECT sum(divideDecimal(d40, toDecimal256(7, 1), 40)) FROM (SELECT d40 FROM div256 LIMIT 50000)</query>
<query>SELECT sum(multiplyDecimal(d40, toDecimal256(7, 1), 40)) FROM (SELECT d40 FROM div256 LIMIT 50000)</query>
<!-- 128 bits is a separate implementation with two limbs, so the operands are either narrow or
full width and nothing in between. Narrow operands are what `Decimal128` arithmetic holds
in practice; `round_methods.xml` covers the rounding shape of that over `Decimal128`. -->
<query>SELECT sum(intDiv(small_u, 997)) FROM div128</query>
<query>SELECT sum(modulo(small_u, 997)) FROM div128</query>
<query>SELECT sum(intDiv(small_u, 1000000000000000000)) FROM div128</query>
<query>SELECT sum(intDiv(small_i, 997)) FROM div128</query>
<query>SELECT sum(intDiv(small_u, small_u + 1)) FROM div128</query>
<!-- Both operands full width, which keeps the two-limb path measured as well. -->
<query>SELECT sum(intDiv(u, toUInt128('100000000000000000000'))) FROM div128</query>
<query>SELECT sum(intDiv(u, small_u + 1)) FROM div128</query>
<query>SELECT sum(intDiv(u, i + 1)) FROM div128</query>
<drop_query>DROP TABLE IF EXISTS div256</drop_query>
<drop_query>DROP TABLE IF EXISTS div128</drop_query>
</test>