mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2026-10-28 16:18:23 +08:00
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
104 lines
5.4 KiB
XML
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>
|