Skip to content

[opt](decimal) fast paths for wide-integer (Decimal256) division - #66391

Open
Baymine wants to merge 1 commit into
apache:masterfrom
Baymine:improvement/wide-integer-divide-fast-paths
Open

[opt](decimal) fast paths for wide-integer (Decimal256) division#66391
Baymine wants to merge 1 commit into
apache:masterfrom
Baymine:improvement/wide-integer-divide-fast-paths

Conversation

@Baymine

@Baymine Baymine commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: no issue

Problem Summary:

wide::integer division (backing Decimal256 and other >128-bit integer types)
always fell back to a generic bit-by-bit binary long-division loop that iterates
~Bits times (256 shift/compare/subtract rounds for a 256-bit value), even when
the operands are small. Decimal256 arithmetic and the CEIL/ROUND x / 10^k
rounding paths hit this hot loop constantly with operands that are far narrower
than 256 bits, so the general algorithm dominates the cost.

This adds three stacked fast paths in front of the generic loop, each bit-exact
with it (verified against native __int128 oracles and via q*d+r==n identities),
and each writing the remainder back into numerator so operator% stays correct:

  1. Both operands fit in 128 bits (the common money/count magnitude): perform a
    single native unsigned __int128 divide. Placed first because it is the
    cheapest and most frequently hit.
  2. Divisor fits in a single 64-bit limb (e.g. x / 10^k): schoolbook word-by-word
    long division, one hardware 128/64 divide per limb -- O(item_count) divides
    instead of ~Bits iterations.
  3. Divisor fits in two 64-bit limbs (65..128-bit divisor): route through a new
    divide_knuth() helper implementing Knuth's Algorithm D (Hacker's Delight
    divmnu) in base 2^32, which keeps every intermediate product within a
    uint64_t and stays overflow-safe.

Divisors wider than 128 bits and the zero-divisor throw are unchanged and fall
through to the existing generic path. On a fast-path miss the only added cost is
a short limb scan.

Release note

None

Check List (For Author)

  • Test: Unit Test
    • Added/extended be/test/core/wide_integer_test.cpp (18 new cases:
      single-limb, two-limb Knuth, both-fit-128, signed, boundary, divide-by-zero,
      and randomized differential/ground-truth fuzz against native __int128).
      All 23 WideInteger tests pass locally (ASAN build).
  • Behavior changed: No (pure performance optimization; results are bit-exact with
    the previous slow path)
  • Does this need documentation: No

### What problem does this PR solve?

Issue Number: no issue

Problem Summary:

`wide::integer` division (backing Decimal256 and other >128-bit integer types)
always fell back to a generic bit-by-bit binary long-division loop that iterates
~Bits times (256 shift/compare/subtract rounds for a 256-bit value), even when
the operands are small. Decimal256 arithmetic and the CEIL/ROUND `x / 10^k`
rounding paths hit this hot loop constantly with operands that are far narrower
than 256 bits, so the general algorithm dominates the cost.

This adds three stacked fast paths in front of the generic loop, each bit-exact
with it (verified against native `__int128` oracles and via q*d+r==n identities),
and each writing the remainder back into `numerator` so `operator%` stays correct:

1. Both operands fit in 128 bits (the common money/count magnitude): perform a
   single native `unsigned __int128` divide. Placed first because it is the
   cheapest and most frequently hit.
2. Divisor fits in a single 64-bit limb (e.g. `x / 10^k`): schoolbook word-by-word
   long division, one hardware 128/64 divide per limb -- O(item_count) divides
   instead of ~Bits iterations.
3. Divisor fits in two 64-bit limbs (65..128-bit divisor): route through a new
   `divide_knuth()` helper implementing Knuth's Algorithm D (Hacker's Delight
   `divmnu`) in base 2^32, which keeps every intermediate product within a
   uint64_t and stays overflow-safe.

Divisors wider than 128 bits and the zero-divisor throw are unchanged and fall
through to the existing generic path. On a fast-path miss the only added cost is
a short limb scan.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - Added/extended `be/test/core/wide_integer_test.cpp` (18 new cases:
      single-limb, two-limb Knuth, both-fit-128, signed, boundary, divide-by-zero,
      and randomized differential/ground-truth fuzz against native __int128).
      All 23 WideInteger tests pass locally (ASAN build).
- Behavior changed: No (pure performance optimization; results are bit-exact with
  the previous slow path)
- Does this need documentation: No
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@Baymine

Baymine commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28797 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit d6a544877fb84c93050fb00eeb9071e7c3554f4d, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17748	4029	3901	3901
q2	1997	315	195	195
q3	10301	1390	822	822
q4	4717	462	332	332
q5	7633	816	554	554
q6	214	162	137	137
q7	726	791	589	589
q8	10439	1594	1618	1594
q9	5400	3978	3990	3978
q10	6795	1647	1353	1353
q11	513	349	318	318
q12	702	585	449	449
q13	18084	3286	2759	2759
q14	256	255	229	229
q15	q16	723	730	657	657
q17	954	981	1028	981
q18	6769	5921	5560	5560
q19	1186	1207	1101	1101
q20	798	691	555	555
q21	5696	2660	2430	2430
q22	420	355	303	303
Total cold run time: 102071 ms
Total hot run time: 28797 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4201	4127	4125	4125
q2	273	313	206	206
q3	4510	4981	4376	4376
q4	2146	2242	1396	1396
q5	4221	4090	4052	4052
q6	220	173	124	124
q7	1674	1614	1372	1372
q8	2804	2046	2043	2043
q9	7153	7135	7121	7121
q10	4321	4299	3870	3870
q11	557	394	366	366
q12	727	713	506	506
q13	3176	3376	2984	2984
q14	309	304	284	284
q15	q16	720	692	617	617
q17	1276	1267	1275	1267
q18	7926	7111	6991	6991
q19	1052	1031	1068	1031
q20	2201	2184	1907	1907
q21	5152	4562	4346	4346
q22	526	469	405	405
Total cold run time: 55145 ms
Total hot run time: 49389 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 168551 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit d6a544877fb84c93050fb00eeb9071e7c3554f4d, data reload: false

query5	4325	595	456	456
query6	451	219	207	207
query7	4823	582	334	334
query8	332	182	166	166
query9	8795	3966	3933	3933
query10	447	355	326	326
query11	5820	2202	2001	2001
query12	148	100	94	94
query13	1283	580	446	446
query14	6151	4671	4347	4347
query14_1	3777	3767	3758	3758
query15	218	203	174	174
query16	1008	493	440	440
query17	1158	670	530	530
query18	2422	456	325	325
query19	198	173	144	144
query20	104	101	105	101
query21	232	155	136	136
query22	12969	13024	12789	12789
query23	17413	16339	15997	15997
query23_1	16086	15988	16049	15988
query24	7580	1694	1185	1185
query24_1	1254	1225	1239	1225
query25	559	425	342	342
query26	1341	351	200	200
query27	2650	565	387	387
query28	4422	2028	2024	2024
query29	1017	599	453	453
query30	337	259	224	224
query31	1116	1049	957	957
query32	94	62	58	58
query33	529	318	264	264
query34	1209	1125	665	665
query35	733	737	630	630
query36	810	762	736	736
query37	160	101	85	85
query38	1824	1608	1570	1570
query39	831	822	776	776
query39_1	802	778	773	773
query40	239	166	140	140
query41	64	63	62	62
query42	89	91	89	89
query43	312	313	275	275
query44	1410	762	774	762
query45	189	173	164	164
query46	1036	1179	716	716
query47	1558	1542	1472	1472
query48	413	406	302	302
query49	572	410	292	292
query50	1051	432	341	341
query51	10567	10431	10175	10175
query52	82	85	72	72
query53	258	261	199	199
query54	292	233	239	233
query55	74	69	65	65
query56	296	295	288	288
query57	1029	1018	984	984
query58	305	258	254	254
query59	1525	1592	1420	1420
query60	299	265	250	250
query61	158	152	152	152
query62	399	315	268	268
query63	226	198	196	196
query64	2878	1183	973	973
query65	3912	3824	3795	3795
query66	1823	485	375	375
query67	28187	28178	28028	28028
query68	3244	1575	1053	1053
query69	414	322	278	278
query70	874	829	801	801
query71	391	342	336	336
query72	3160	2796	2255	2255
query73	847	771	441	441
query74	4597	4448	4265	4265
query75	2371	2351	1981	1981
query76	2348	1119	769	769
query77	321	369	261	261
query78	11221	11112	10718	10718
query79	1382	1188	752	752
query80	1160	541	465	465
query81	503	334	282	282
query82	564	143	113	113
query83	398	318	294	294
query84	271	153	130	130
query85	977	600	573	573
query86	363	241	222	222
query87	1805	1785	1697	1697
query88	3743	2826	2800	2800
query89	409	321	282	282
query90	1817	191	195	191
query91	203	189	165	165
query92	60	62	57	57
query93	1536	1572	918	918
query94	615	356	305	305
query95	776	499	461	461
query96	1095	829	339	339
query97	2451	2483	2342	2342
query98	205	193	190	190
query99	723	726	613	613
Total cold run time: 255546 ms
Total hot run time: 168551 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 23.74 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit d6a544877fb84c93050fb00eeb9071e7c3554f4d, data reload: false

query1	0.01	0.01	0.00
query2	0.09	0.05	0.05
query3	0.25	0.14	0.13
query4	1.61	0.14	0.14
query5	0.25	0.22	0.21
query6	1.16	0.81	0.81
query7	0.04	0.01	0.01
query8	0.05	0.04	0.04
query9	0.37	0.30	0.32
query10	0.54	0.53	0.54
query11	0.20	0.14	0.13
query12	0.18	0.15	0.14
query13	0.46	0.47	0.46
query14	1.01	0.99	0.98
query15	0.59	0.60	0.58
query16	0.30	0.33	0.31
query17	1.10	1.04	1.02
query18	0.21	0.19	0.19
query19	2.07	1.97	1.94
query20	0.02	0.01	0.02
query21	15.44	0.22	0.13
query22	4.80	0.06	0.05
query23	16.14	0.30	0.12
query24	2.92	0.40	0.33
query25	0.11	0.05	0.04
query26	0.73	0.22	0.16
query27	0.06	0.03	0.05
query28	3.44	0.80	0.35
query29	12.50	4.00	3.18
query30	0.28	0.16	0.15
query31	2.77	0.55	0.32
query32	3.22	0.58	0.48
query33	3.22	3.22	3.17
query34	15.57	3.89	3.27
query35	3.19	3.20	3.20
query36	0.54	0.42	0.43
query37	0.08	0.06	0.06
query38	0.05	0.04	0.04
query39	0.03	0.02	0.02
query40	0.18	0.15	0.15
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 95.95 s
Total hot run time: 23.74 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 98.79% (163/165) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 59.28% (25714/43375)
Line Coverage 43.39% (258448/595661)
Region Coverage 39.07% (204918/524443)
Branch Coverage 40.41% (93492/231362)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 98.79% (163/165) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 75.82% (32073/42301)
Line Coverage 60.56% (358293/591659)
Region Coverage 57.17% (300837/526201)
Branch Coverage 58.57% (135531/231396)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants