Skip to content

[improvement](be) Optimize bit unpacking with PDEP#65738

Open
HappenLee wants to merge 6 commits into
apache:masterfrom
HappenLee:improvement-pdep-unpack
Open

[improvement](be) Optimize bit unpacking with PDEP#65738
HappenLee wants to merge 6 commits into
apache:masterfrom
HappenLee:improvement-pdep-unpack

Conversation

@HappenLee

@HappenLee HappenLee commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary:

BitPacking::UnpackValues currently decodes every complete 32-value batch with scalar shifts and masks. This PR adds an x86 PDEP implementation for uint8_t, uint16_t, and uint32_t, with AVX2 widening for narrow uint16_t and uint32_t values.

The optimized functions are compiled with target("bmi2,avx2"). Runtime dispatch uses __builtin_cpu_supports("bmi2") and __builtin_cpu_supports("avx2"); unsupported CPUs, architectures, output types, and remainder values continue to use the generic scalar implementation. Doris therefore does not require a global -mbmi2 build flag.

Production dispatch deliberately uses PDEP only when BIT_WIDTH < 16. The generic PDEP implementation remains available to the benchmark for all supported widths, but repeated measurements found non-monotonic, CPU- and working-set-dependent results at 16-32 bits. A detailed code comment explains why the scalar implementation is retained for these widths instead of adding an irregular CPU-specific allowlist.

The PR also adds unit tests for every supported bit width, including full batches and truncated/remainder input, plus a reproducible benchmark comparing:

  • the generic scalar kernel;
  • the direct PDEP kernel;
  • the actual BitPacking::UnpackValues dispatch path.

The benchmark covers 4K, 256K, and 1M values and validates optimized output against the scalar reference before measuring.

Benchmark results

Hardware: Intel Xeon Platinum 8457C, 48 KiB L1D and 2 MiB private L2 per core. Release build pinned to one CPU. Values below are CPU-time medians in microseconds.

L1-sized working set: 4K uint32_t values, measured with 9 randomized repetitions. Each iteration touches a 16 KiB output and 1-7 KiB of packed input; including the benchmark's reference output, all buffers total 33-39 KiB and fit in the 48 KiB L1D.

bit width scalar PDEP speedup
2 0.628 0.338 1.86x
4 0.551 0.336 1.64x
6 0.969 0.355 2.73x
8 0.224 0.221 1.01x
10 1.216 0.565 2.15x
12 0.870 0.564 1.54x
14 0.828 0.737 1.12x

L2-sized working set: 256K uint32_t values. The output is 1 MiB and the packed input is 64-448 KiB.

bit width scalar PDEP speedup
2 41.0 27.9 1.47x
4 38.5 29.0 1.33x
6 61.6 28.6 2.15x
8 32.5 31.5 1.03x
10 77.6 36.0 2.16x
12 51.1 35.9 1.42x
14 50.6 47.1 1.07x

The result is workload- and bit-width-dependent. PDEP is faster for all measured widths while the working set remains in L1 or L2, although the gains at widths 8 and 14 are marginal. At 1M values, the actual PDEP path is faster than the scalar path at widths 10, 12, and 14, but slower at widths 2, 4, 6, and 8.

High-width dispatch validation

Widths 16-32 were measured twice independently. The following table is the second run with 9 randomized repetitions; scalar/PDEP greater than 1 means PDEP is faster.

bit width 256K scalar 256K PDEP scalar/PDEP 1M scalar 1M PDEP scalar/PDEP
16 27.84 55.89 0.498x 234.49 273.85 0.856x
17 62.60 60.04 1.043x 253.42 255.63 0.991x
18 65.04 56.52 1.151x 263.04 258.88 1.016x
19 77.90 59.92 1.300x 313.56 261.44 1.199x
20 63.27 55.17 1.147x 283.19 264.64 1.070x
21 67.23 58.28 1.154x 277.33 269.93 1.027x
22 70.41 57.38 1.227x 284.12 271.71 1.046x
23 68.79 68.49 1.004x 281.42 289.78 0.971x
24 73.75 56.84 1.297x 342.26 278.40 1.229x
25 67.05 81.08 0.827x 291.52 330.73 0.881x
26 69.09 79.70 0.867x 290.12 324.68 0.894x
27 66.78 61.01 1.095x 285.06 293.61 0.971x
28 63.18 75.05 0.842x 290.85 310.27 0.937x
29 64.87 58.74 1.104x 295.88 304.93 0.970x
30 64.47 60.25 1.070x 303.09 307.15 0.987x
31 53.94 66.08 0.816x 316.73 310.85 1.019x
32 42.03 71.03 0.592x 297.63 333.45 0.893x

The first independent run showed the same material regressions at widths 16, 25, 26, 28, and 32. The uint16_t width-16 boundary was also slower with PDEP at 4K, 256K, and 1M values. Some high widths improve, but the profitable set changes with the working set and has no monotonic boundary. The production path therefore conservatively retains scalar unpacking for all widths at or above 16. After this change, the actual entry point was benchmarked again: width 15 follows direct PDEP, while widths 16-32 follow the scalar path and all outputs match the scalar reference.

Release note

Improve bit-packed integer decoding for bit widths below 16 on BMI2 and AVX2 capable x86 CPUs.

Check List (For Author)

  • Test:
    • Unit Test: ./run-be-ut.sh -j 48 --run --filter=BitPackingTest.*
    • Manual test: compiled and linked benchmark_test; ran scalar, direct PDEP, and actual-path cases at 4K, 256K, and 1M values
    • build-support/check-format.sh
    • build-support/run-clang-tidy.sh --build-dir be/build_Release --files be/benchmark/benchmark_main.cpp be/test/util/bit_packing_test.cpp
  • Behavior changed: Yes. Supported x86 CPUs use PDEP for complete 32-value batches only when BIT_WIDTH < 16; all other cases retain the scalar path.
  • Does this need documentation: No

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: BitPacking decoded every full 32-value batch with scalar shifts and masks even on x86 CPUs that support BMI2 and AVX2. Add a PDEP-based unpacker for uint8_t, uint16_t, and uint32_t batches, use AVX2 widening for narrow values, and retain the existing scalar path for unsupported CPUs, architectures, types, and remainder values.

### Release note

Improve bit-packed integer decoding performance on BMI2 and AVX2 capable x86 CPUs.

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j 48 --run --filter=BitPackingTest.*
- Behavior changed: Yes, full 32-value bit-unpack batches use PDEP and AVX2 when supported; decoded results and fallback behavior are unchanged.
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: Add a benchmark that compares scalar 32-value bit unpacking with the production PDEP and AVX2 implementation for uint8_t, uint16_t, and uint32_t across every supported bit width. Validate scalar and PDEP outputs before measuring either implementation.

### Release note

None

### Check List (For Author)

- Test: Manual test
    - ./build.sh --benchmark -j 48
    - benchmark_test --benchmark_filter=BM_.*Unpack --benchmark_min_time=0.01s
    - build-support/run-clang-tidy.sh --build-dir be/build_RELEASE --files be/benchmark/benchmark_main.cpp
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: Parameterize the PDEP unpack benchmark with 4K, 256K, and 1M values, compare the scalar kernel, direct PDEP kernel, and the actual BitPacking entry point, and validate optimized output against the scalar reference before measuring.

### Release note

None

### Check List (For Author)

- Test: Manual test
    - Built benchmark_test and ran the uint32_t 256K/1M scalar, PDEP, and actual-path benchmark cases
- Behavior changed: No
- 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?

### What problem does this PR solve?

Issue Number: None

Related PR: apache#65738

Problem Summary: The generic PDEP unpack implementation remains functionally valid for 16-32 bit values, but repeated Release benchmarks at 4K, 256K, and 1M values showed non-monotonic performance and repeatable regressions at several high widths. Keep the generic implementation available to the benchmark, while limiting the production dispatch to BIT_WIDTH < 16. Wider values conservatively retain the scalar implementation instead of using a CPU- and working-set-specific width allowlist.

### Release note

Use the PDEP bit-unpack optimization only for bit widths below 16.

### Check List (For Author)

- Test: Unit Test / Manual test
    - ./run-be-ut.sh -j 48 --run --filter=BitPackingTest.*
    - Release benchmark_test for uint32_t widths 16-32 at 4K, 256K, and 1M values
    - build-support/check-format.sh
    - build-support/run-clang-tidy.sh --build-dir be/build_Release --files be/benchmark/benchmark_main.cpp be/test/util/bit_packing_test.cpp
- Behavior changed: Yes, bit widths 16 and above retain the scalar unpack path
- Does this need documentation: No
@HappenLee

Copy link
Copy Markdown
Contributor Author

/review

@HappenLee

Copy link
Copy Markdown
Contributor Author

run buildall

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review result: request changes for one blocking performance regression.

Critical checkpoint conclusions:

  • Goal and correctness: The patch is focused and the PDEP layouts, exact batch bounds, returned cursor, truncated input, scalar remainder, and unsupported-CPU fallback all match the existing decoding contract in code review and independent all-width models. However, the optimization goal is not met across Doris's supported x86 population because the feature-only dispatch in MAIN-01 enables a severe regression on pre-Zen3 AMD CPUs.
  • Scope and parallel paths: The five changed files are cohesive. The production chain through BatchedBitReader, Parquet booleans, and RleBatchDecoder was traced; the separate dictionary-decoding API is unchanged and does not require a semantic parallel update.
  • Concurrency, lifecycle, memory, and errors: The kernel is stateless, owns no memory, adds no threads/locks/static-lifetime dependencies, and preserves decoder short-read failure behavior. No concurrency, lifecycle, MemTracker, or Status issue was found.
  • Configuration, compatibility, persistence, and transactions: No configuration, persisted/storage format, FE-BE protocol, transaction, or rolling-upgrade boundary changes are involved; non-x86 builds, compilers other than GCC/Clang, and CPUs without BMI2+AVX2 retain the scalar implementation.
  • Performance: MAIN-01 is blocking. A CPU-family profitability gate and representative AMD benchmarking are required before capability-only PDEP dispatch is safe.
  • Tests and results: The new BE unit test is registered and covers all widths, two full batches plus a remainder, and a truncated buffer. It does not force the optimized ISA path on a feature-lacking runner, and the direct benchmark is opt-in and Intel-only; add capability-gated direct coverage and representative AMD performance coverage. Per the review-run contract I did not build or run tests locally; formatter checks were green, while the observed macOS BE-UT failure occurred before compilation because that runner selected JDK 25 instead of JDK 17.
  • Observability and user focus: No additional logging or metrics are needed for this local stateless kernel. No additional user-provided review focus was supplied.

Review completion: complete after two-agent full coverage, a separate risk-focused pass, deduplication, and convergence. One inline issue is submitted; no other unresolved suspicious point remains.

Comment thread be/src/util/pdep_unpack.h Outdated
@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17701	4148	4233	4148
q2	2031	353	207	207
q3	10284	1429	887	887
q4	4701	479	342	342
q5	7489	853	575	575
q6	182	179	137	137
q7	779	812	608	608
q8	9325	1654	1561	1561
q9	6093	4364	4327	4327
q10	6759	1722	1475	1475
q11	519	355	332	332
q12	731	593	463	463
q13	18144	3469	2813	2813
q14	267	257	254	254
q15	q16	788	778	704	704
q17	931	923	1011	923
q18	7160	5941	5518	5518
q19	1164	1301	1021	1021
q20	811	699	559	559
q21	5619	2583	2423	2423
q22	435	363	297	297
Total cold run time: 101913 ms
Total hot run time: 29574 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4488	4377	4342	4342
q2	281	321	213	213
q3	4568	4949	4406	4406
q4	2050	2153	1363	1363
q5	4446	4326	4326	4326
q6	228	174	127	127
q7	1728	2199	1780	1780
q8	2536	2249	2182	2182
q9	8135	8071	7840	7840
q10	4712	4640	4192	4192
q11	571	430	395	395
q12	737	782	539	539
q13	3353	3618	2886	2886
q14	297	313	272	272
q15	q16	747	735	635	635
q17	1340	1326	1522	1326
q18	7911	7371	7257	7257
q19	1205	1098	1088	1088
q20	2216	2210	1954	1954
q21	5210	4527	4406	4406
q22	519	450	418	418
Total cold run time: 57278 ms
Total hot run time: 51947 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177350 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 48c64e196344b78b411df4508db91bd9a2cac564, data reload: false

query5	4319	639	494	494
query6	462	230	197	197
query7	4872	585	346	346
query8	341	189	175	175
query9	8773	4003	3994	3994
query10	491	360	303	303
query11	5914	2292	2093	2093
query12	157	102	100	100
query13	1325	622	432	432
query14	6186	5194	4891	4891
query14_1	4355	4261	4228	4228
query15	212	213	175	175
query16	1012	468	429	429
query17	903	704	575	575
query18	2422	478	340	340
query19	207	184	147	147
query20	111	103	102	102
query21	229	153	132	132
query22	13536	13563	13335	13335
query23	17335	16568	16106	16106
query23_1	16331	16232	16228	16228
query24	7440	1771	1258	1258
query24_1	1303	1293	1289	1289
query25	540	474	403	403
query26	1349	362	211	211
query27	2683	582	378	378
query28	4525	1976	1951	1951
query29	1093	629	511	511
query30	336	264	223	223
query31	1109	1087	975	975
query32	121	63	62	62
query33	533	330	270	270
query34	1203	1136	663	663
query35	790	776	687	687
query36	1229	1215	1073	1073
query37	153	106	105	105
query38	1871	1704	1670	1670
query39	904	870	855	855
query39_1	872	838	843	838
query40	243	174	149	149
query41	72	70	75	70
query42	95	94	93	93
query43	328	328	289	289
query44	1426	794	770	770
query45	194	183	189	183
query46	1096	1197	728	728
query47	2174	2053	2050	2050
query48	413	430	295	295
query49	596	433	319	319
query50	1082	453	342	342
query51	10536	10650	10444	10444
query52	88	88	76	76
query53	265	292	209	209
query54	295	251	238	238
query55	79	76	68	68
query56	296	321	320	320
query57	1326	1297	1217	1217
query58	313	268	270	268
query59	1644	1707	1514	1514
query60	319	284	271	271
query61	176	171	175	171
query62	535	497	437	437
query63	252	205	240	205
query64	2811	1023	841	841
query65	4691	4734	4640	4640
query66	1845	502	374	374
query67	29356	28600	29066	28600
query68	3007	1522	1002	1002
query69	407	311	268	268
query70	1082	932	957	932
query71	380	336	313	313
query72	3063	2681	2343	2343
query73	834	764	420	420
query74	5072	4904	4710	4710
query75	2532	2510	2131	2131
query76	2348	1198	783	783
query77	340	368	281	281
query78	11967	11826	11497	11497
query79	1411	1122	761	761
query80	1297	542	452	452
query81	510	339	289	289
query82	591	153	116	116
query83	394	317	295	295
query84	326	161	134	134
query85	972	648	518	518
query86	424	276	288	276
query87	1840	1817	1749	1749
query88	3707	2794	2760	2760
query89	427	376	320	320
query90	1899	203	192	192
query91	204	187	160	160
query92	63	63	53	53
query93	1603	1517	945	945
query94	734	357	320	320
query95	797	594	463	463
query96	999	799	370	370
query97	2619	2616	2484	2484
query98	216	205	201	201
query99	1091	1125	977	977
Total cold run time: 263596 ms
Total hot run time: 177350 ms

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.10	0.05	0.05
query3	0.26	0.14	0.14
query4	1.62	0.14	0.14
query5	0.28	0.23	0.22
query6	1.25	1.09	1.09
query7	0.04	0.01	0.01
query8	0.06	0.04	0.04
query9	0.39	0.32	0.31
query10	0.59	0.56	0.53
query11	0.19	0.13	0.14
query12	0.19	0.14	0.15
query13	0.49	0.49	0.48
query14	1.01	1.01	1.01
query15	0.62	0.60	0.59
query16	0.33	0.33	0.32
query17	1.15	1.12	1.14
query18	0.24	0.21	0.22
query19	2.02	1.94	1.98
query20	0.02	0.01	0.01
query21	15.44	0.21	0.15
query22	4.99	0.06	0.05
query23	16.13	0.31	0.12
query24	2.94	0.43	0.32
query25	0.10	0.06	0.04
query26	0.75	0.21	0.14
query27	0.05	0.04	0.04
query28	3.57	1.00	0.54
query29	12.49	4.14	3.34
query30	0.28	0.15	0.16
query31	2.77	0.63	0.31
query32	3.24	0.59	0.49
query33	3.24	3.21	3.18
query34	15.56	4.39	3.51
query35	3.53	3.56	3.56
query36	0.57	0.44	0.44
query37	0.09	0.07	0.07
query38	0.06	0.04	0.04
query39	0.05	0.03	0.03
query40	0.19	0.16	0.15
query41	0.08	0.04	0.03
query42	0.04	0.03	0.03
query43	0.05	0.04	0.03
Total cold run time: 97.06 s
Total hot run time: 25.24 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 85.22% (98/115) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 57.41% (23928/41680)
Line Coverage 41.13% (235155/571759)
Region Coverage 37.06% (186299/502668)
Branch Coverage 38.11% (83290/218554)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 82.65% (81/98) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 74.91% (30458/40660)
Line Coverage 59.00% (335283/568245)
Region Coverage 55.70% (281063/504566)
Branch Coverage 56.96% (124573/218696)

### What problem does this PR solve?

Issue Number: None

Related PR: apache#65738

Problem Summary: PDEP is supported by AMD Zen+ and Zen 2, but those CPUs execute it as a slow microcoded instruction. Add a BE configuration switch so deployments on those CPUs can retain the scalar bit-unpacking path, while keeping BMI2 optimized paths enabled by default. The generic BMI2 name also allows future BMI2 instruction optimizations to share the same deployment-level switch.

### Release note

Set enable_bmi2_optimizations=false on CPUs where BMI2 optimizations are slower than scalar implementations.

### Check List (For Author)

- Test: Unit Test

    - Compiled config.cpp and BitPackingTest objects in ASAN-UT.

    - Added a unit test that verifies enable_bmi2_optimizations=false disables PDEP dispatch.

- Behavior changed: Yes (adds a BE configuration switch; default behavior remains enabled)

- Does this need documentation: No
@HappenLee
HappenLee force-pushed the improvement-pdep-unpack branch from 63b42ec to cb53beb Compare July 17, 2026 03:23
@HappenLee

Copy link
Copy Markdown
Contributor Author

run buildall

@HappenLee

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review result: request changes for two performance regressions.

Critical checkpoint conclusions:

  • Goal and proof: The PR is focused on accelerating complete 32-value bit-unpack batches, and the new kernel's masks, lane ordering, exact input bounds, returned cursor, truncated tail, and unsupported-target fallback preserve scalar results. The goal is not fully met because production dispatch knowingly selects the PR-reported large-workload regressions and adds dispatch work where no full batch exists.
  • Scope and parallel paths: The seven changed files are cohesive. The complete call chain through BatchedBitReader, Parquet plain booleans, dictionary hybrid-RLE, normal materialization, and sparse external row fetching was reviewed. The two inline findings cover the only substantiated parallel-path gaps.
  • Concurrency and lifecycle: The kernel is stateless, adds no thread, lock, atomic, owned allocation, or cross-TU initialization dependency. The startup-loaded non-mutable BMI2 config is appropriate for fixed CPU capability and introduces no separate race or lifecycle defect.
  • Error, memory, and data correctness: No ignored Status, exception-boundary, MemTracker, ownership, bounds, lane-order, or pointer-accounting defect was found. The change does not affect transaction visibility, persistence, storage formats, data writes, FE-BE variables, or rolling-upgrade compatibility.
  • Configuration and compatibility: GCC/Clang x86 builds use target attributes plus runtime BMI2/AVX2 checks; non-x86, unsupported compilers, and unsupported CPUs retain scalar behavior. The default-on slow-PDEP AMD concern remains the already-known discussion r3598211019 and was not duplicated here.
  • Performance: MAIN-001 is blocking because a valid sparse Parquet dictionary skip can reproduce the supplied 1M-uint32_t width-2/4/6/8 regression even on the benchmarked Intel CPU. MAIN-002 removes deterministic config/feature probes from valid 8/16/24-value RLE literals that cannot execute a PDEP batch.
  • Tests and results: The added tests cover all widths, full batches plus a remainder, truncation, and the config opt-out on capable x86; the direct benchmark validates PDEP against scalar. Intrinsic execution remains capability-dependent and the truncated case could be stronger, but no separate product defect was established. Per the review contract, no local build or test was attempted. Formatter checks are green; the observed macOS BE-UT failure occurs before compilation because the runner selected JDK 25 instead of JDK 17, while auth-gated TeamCity failures/pending reruns were not treated as code evidence.
  • Observability and user focus: No new logging or metrics are needed for this local stateless kernel. No additional user-provided review focus was supplied.

Review completion: complete after a main-agent full scan, two normal full-review subagents plus a separate risk-focused subagent, independent candidate verification/deduplication, and Round 2 same-state convergence. Two inline issues are submitted; every other suspicious point has a concrete dismissed or duplicate conclusion.

Comment thread be/src/util/pdep_unpack.h
// repeatable regressions for multiple high widths. Because the profitable high widths are
// CPU- and working-set-dependent, an irregular per-width allowlist would not be portable;
// use the scalar implementation conservatively instead.
return is_supported_type<T, BIT_WIDTH>() && BIT_WIDTH < 16;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep known large low-width regressions on scalar

should_use() enables every supported width below 16, but the PR's own 1M-uint32_t results say widths 2, 4, 6, and 8 are slower on the benchmarked Intel Xeon. That working set is reachable: sparse external Parquet row fetches create singleton ranges; a large within-page gap goes through BaseDictDecoder::skip_values() and its RleBatchDecoder<uint32_t> forwards literal indices directly to UnpackBatch(), with no 65,535-row cap. Thus valid dictionary skips select a known regression even on fast-PDEP Intel, distinct from the existing AMD concern. Please gate on num_values/working set or retain scalar for combinations already shown to regress.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anchor correction for the second finding from review 4719268390; the comprehensive checkpoint summary remains there. This review places the unchanged MAIN-002 body on the exact is_supported() call after Round 3 same-state convergence.

Comment thread be/src/util/bit_packing.inline.h Outdated
#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
int64_t batches_read = 0;
if constexpr (PdepUnpack::should_use<OutType, BIT_WIDTH>()) {
if (PdepUnpack::is_supported()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Skip feature probes when no full batch exists

For values_to_read < 32, batches_to_read is zero, yet this still loads the config and probes BMI2/AVX2 before falling through to the unchanged scalar remainder. Parquet hybrid-RLE legitimately sends 8-, 16-, and 24-value literal tails from FillLiteralBuffer() here, so this adds dispatch overhead to a hot path with no possibility of using PDEP, and the benchmark starts at 4,096 values so it does not cover it. Guard is_supported() with batches_to_read > 0 so short runs remain unchanged.

### What problem does this PR solve?

Issue Number: None

Related PR: apache#65738

Problem Summary: BitPacking::UnpackValues called PdepUnpack::is_supported() even when fewer than 32 values were available. Parquet hybrid-RLE can send 8-, 16-, and 24-value literal tails through this path, so these scalar-only calls unnecessarily loaded the BMI2 configuration and probed BMI2/AVX2 support. Short-circuit the feature probe when batches_to_read is zero so partial batches continue directly to the unchanged scalar remainder decoder.

### Release note

None

### Check List (For Author)

- Test: Unit Test / Static Analysis
    - Compiled be/ut_build_ASAN/test/CMakeFiles/doris_be_test.dir/util/bit_packing_test.cpp.o
    - Attempted ./run-be-ut.sh -j 48 --run --filter=BitPackingTest.*; stopped because the local CMake state triggered an unrelated 2,259-target full rebuild
    - PATH=/mnt/disk6/common/ldb_toolchain_toucan/bin:$PATH build-support/check-format.sh
    - build-support/run-clang-tidy.sh --build-dir be/ut_build_ASAN --files be/src/util/bit_packing.inline.h
- Behavior changed: No (partial batches keep the scalar decoder while avoiding unnecessary runtime feature dispatch)
- Does this need documentation: No
@HappenLee

Copy link
Copy Markdown
Contributor Author

/review

@HappenLee

Copy link
Copy Markdown
Contributor Author

run buildall

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review result: request changes for two additional blocking performance-validation issues.

The optimized bit layouts, buffer bounds, byte consumption, scalar remainder handoff, and architecture fallback are internally consistent. However, the current benchmark process leaves the new config flag zero-initialized, so direct PDEP cases skip and actual-path cases measure scalar; separately, production selects the marginal width-8 kernel for common exact-one-batch calls that the benchmark does not cover and whose published direct-kernel margin cannot pay the per-call dispatch. The inline comments contain the concrete evidence and fix direction. Existing AMD, large-working-set, and zero-batch threads were treated as known context and were not duplicated.

Critical checkpoint conclusions:

  • Goal and proof: The PR aims to accelerate narrow bit unpacking. Functional parity is well covered by all-width and truncated-input tests, but the performance goal is not established for current-head benchmark execution or one/few-batch production calls; MAIN-001 and MAIN-002 block approval.
  • Scope and reuse: The production change is localized to UnpackValues plus a focused kernel/config/benchmark/test addition. The scalar implementation is reused for unsupported types, widths, CPUs, architectures, and remainders.
  • Concurrency: No new worker, lock, shared mutable runtime state, or atomic protocol is introduced. The production config is immutable after startup; the unit test changes it sequentially and restores it.
  • Lifecycle/static initialization: Production config initialization precedes decoder use, and a pre-init read would conservatively select scalar. The standalone benchmark omits config initialization, which is MAIN-001. No circular ownership or cross-TU initializer correctness dependency remains.
  • Configuration: A non-dynamic deployment switch is appropriate for immutable CPU characteristics, but benchmark startup must assign its registered default before using it.
  • Compatibility and parallel paths: No wire, storage, symbol, FE/BE, rolling-upgrade, transaction, persistence, or data-write format changes apply. Raw unpack callers were traced through BatchedBitReader, Parquet RLE dictionary indices, and plain booleans; dictionary-decoding APIs remain intentionally scalar and semantically unchanged.
  • Conditions and error handling: CPU/width/architecture guards safely retain scalar behavior, and no Status or exception path is added. Batch-count/width performance selection is still too broad for the demonstrated workloads.
  • Tests and results: The added unit tests cover all supported widths, full batches, truncated exact boundaries, remainders, and the disable flag; optimized execution remains CPU-feature-dependent. The benchmark compares scalar/direct/actual outputs but currently cannot enable the new flag and omits 32/64/128-value actual-path cases. Per the review contract, this invocation performed static/model validation and did not run builds.
  • Observability: No new operational metric or log is required for this local decoding kernel; the deployment config supplies a fallback control.
  • Performance and memory: The kernel adds no allocation or MemTracker concern, and large-buffer correctness is sound. The unresolved issues are invalid current benchmark dispatch and a substantiated exact-one-batch regression; existing large-working-set and slow-CPU concerns remain in their original threads.
  • Other correctness: Independent randomized models and source proofs found exact scalar parity, bounded 8/9-byte loads, exactly 32 stores, correct input/output advancement, and correct remainder behavior across production and benchmark-only widths.
  • User focus: No additional user-provided focus was specified; the full PR was reviewed.

Review workflow: the main risk scan, normal full reviews, risk-focused reviews, candidate deduplication, independent verification, and same-state convergence were completed on head 790ad0cafb05c0a778c779794161892b8011846f; every suspicious point has an accepted, dismissed-with-evidence, or duplicate conclusion.

void BM_PdepUnpack(benchmark::State& state) {
const int bit_width = static_cast<int>(state.range(0));
const int num_values = static_cast<int>(state.range(1));
if (!PdepUnpack::is_supported()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Initialize config before running these benchmarks

DEFINE_Bool leaves its namespace-scope storage zero-initialized; Register only records the "true" default, and config::init() is what later assigns it. benchmark_main.cpp never calls config::init(), so this predicate is always false in the benchmark process: every direct PDEP case is skipped, while every BM_ActualUnpack case silently measures the scalar fallback. Because the config guard was added after the published runs, the current benchmark can no longer reproduce the evidence used to select production dispatch. Initialize registered defaults before RunSpecifiedBenchmarks() (or explicitly enable this flag for the direct/actual cases) and verify that the actual-path cases enter PDEP on capable hardware.

#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
int64_t batches_read = 0;
if constexpr (PdepUnpack::should_use<OutType, BIT_WIDTH>()) {
if (batches_to_read > 0 && PdepUnpack::is_supported()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep marginal one-batch calls on the scalar path

The new batches_to_read > 0 guard fixes the already-reported zero-batch probe, but exact 32-value calls are a separate normal production shape: RleBatchDecoder can send one batch through its rounded direct bypass, and FillLiteralBuffer() is fixed at 32 values. For uint32_t width 8, the PR reports only 0.224 us scalar versus 0.221 us PDEP over 4,096 values—3 ns across 128 kernels, or about 0.023 ns saved per kernel before this per-call config load, two feature tests, and branch. An exact-one-batch call therefore cannot amortize the new dispatch, while the benchmark starts at 4,096 values and never measures 32/64/128-value actual-entry calls. Add those cases and retain scalar for one/few batches at marginal widths (at least width 8) unless end-to-end results show a win.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17738	4172	4132	4132
q2	2042	326	195	195
q3	10289	1468	842	842
q4	4685	478	339	339
q5	7501	847	568	568
q6	185	168	137	137
q7	766	844	614	614
q8	9436	1576	1606	1576
q9	5636	4423	4390	4390
q10	6777	1745	1488	1488
q11	508	349	324	324
q12	725	595	460	460
q13	18163	3445	2745	2745
q14	269	263	248	248
q15	q16	785	780	719	719
q17	1016	972	922	922
q18	6952	5816	5678	5678
q19	1299	1220	1085	1085
q20	849	705	593	593
q21	6409	2910	2705	2705
q22	451	375	306	306
Total cold run time: 102481 ms
Total hot run time: 30066 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5193	4780	4768	4768
q2	294	336	213	213
q3	4943	5281	4798	4798
q4	2088	2168	1375	1375
q5	4930	4604	4665	4604
q6	234	173	127	127
q7	1864	1765	1535	1535
q8	2422	2137	2133	2133
q9	7654	7213	7195	7195
q10	4652	4591	4184	4184
q11	531	385	349	349
q12	717	749	523	523
q13	2993	3300	2756	2756
q14	280	292	265	265
q15	q16	670	699	636	636
q17	1290	1271	1258	1258
q18	7464	6836	6986	6836
q19	1118	1078	1071	1071
q20	2212	2218	1933	1933
q21	5298	4579	4422	4422
q22	514	451	388	388
Total cold run time: 57361 ms
Total hot run time: 51369 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177757 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 790ad0cafb05c0a778c779794161892b8011846f, data reload: false

query5	4364	610	479	479
query6	459	241	209	209
query7	4882	550	316	316
query8	334	193	171	171
query9	8766	4046	4012	4012
query10	496	368	318	318
query11	5863	2375	2094	2094
query12	158	106	100	100
query13	1265	603	421	421
query14	6257	5229	4852	4852
query14_1	4239	4226	4217	4217
query15	214	213	179	179
query16	1017	495	461	461
query17	961	719	580	580
query18	2455	480	355	355
query19	209	190	155	155
query20	114	109	108	108
query21	249	167	136	136
query22	13533	13562	13419	13419
query23	17288	16524	16234	16234
query23_1	16290	16194	16359	16194
query24	7510	1759	1266	1266
query24_1	1300	1287	1299	1287
query25	564	470	396	396
query26	1346	352	208	208
query27	2646	613	399	399
query28	4438	1999	1969	1969
query29	1095	624	498	498
query30	337	266	235	235
query31	1113	1096	978	978
query32	113	63	64	63
query33	551	312	253	253
query34	1155	1131	635	635
query35	756	778	674	674
query36	1211	1196	1066	1066
query37	151	103	86	86
query38	1879	1707	1648	1648
query39	876	876	860	860
query39_1	853	827	852	827
query40	258	160	141	141
query41	65	62	62	62
query42	94	89	95	89
query43	317	318	275	275
query44	1398	791	752	752
query45	201	193	175	175
query46	1118	1184	744	744
query47	2175	2122	2024	2024
query48	389	367	297	297
query49	573	405	313	313
query50	1047	439	331	331
query51	10730	10859	10705	10705
query52	87	88	77	77
query53	268	272	204	204
query54	275	233	216	216
query55	75	70	67	67
query56	323	291	285	285
query57	1322	1295	1218	1218
query58	298	246	258	246
query59	1558	1665	1427	1427
query60	291	276	251	251
query61	151	146	151	146
query62	541	496	432	432
query63	240	203	206	203
query64	2849	1032	836	836
query65	4730	4632	4623	4623
query66	1824	495	380	380
query67	29388	29254	29168	29168
query68	3255	1516	1002	1002
query69	425	299	279	279
query70	1048	946	949	946
query71	368	353	302	302
query72	3180	2707	2363	2363
query73	809	715	403	403
query74	5037	4911	4728	4728
query75	2558	2504	2132	2132
query76	2321	1197	766	766
query77	344	371	281	281
query78	11776	11794	11323	11323
query79	1365	1099	748	748
query80	745	546	450	450
query81	472	331	288	288
query82	558	156	122	122
query83	379	325	296	296
query84	335	158	129	129
query85	978	590	501	501
query86	399	299	277	277
query87	1821	1819	1826	1819
query88	3682	2767	2756	2756
query89	440	377	322	322
query90	1951	192	196	192
query91	201	186	161	161
query92	60	60	55	55
query93	1526	1511	984	984
query94	613	360	294	294
query95	784	508	452	452
query96	1123	803	349	349
query97	2641	2601	2502	2502
query98	214	203	201	201
query99	1098	1113	975	975
Total cold run time: 262702 ms
Total hot run time: 177757 ms

@hello-stephen

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

query1	0.01	0.00	0.01
query2	0.12	0.05	0.05
query3	0.26	0.13	0.14
query4	1.62	0.14	0.14
query5	0.24	0.22	0.22
query6	1.22	1.09	1.06
query7	0.04	0.01	0.00
query8	0.05	0.04	0.04
query9	0.38	0.31	0.31
query10	0.56	0.58	0.55
query11	0.19	0.13	0.14
query12	0.18	0.14	0.14
query13	0.46	0.47	0.47
query14	1.03	1.02	1.00
query15	0.60	0.59	0.59
query16	0.32	0.33	0.32
query17	1.07	1.10	1.08
query18	0.23	0.20	0.20
query19	1.99	2.00	1.97
query20	0.02	0.01	0.01
query21	15.43	0.22	0.12
query22	4.91	0.05	0.05
query23	16.16	0.31	0.13
query24	2.93	0.40	0.32
query25	0.11	0.06	0.05
query26	0.72	0.19	0.15
query27	0.04	0.04	0.03
query28	3.52	0.90	0.54
query29	12.51	4.11	3.29
query30	0.28	0.15	0.16
query31	2.77	0.59	0.31
query32	3.24	0.59	0.50
query33	3.20	3.19	3.14
query34	15.54	4.18	3.54
query35	3.50	3.49	3.54
query36	0.56	0.42	0.44
query37	0.09	0.06	0.06
query38	0.06	0.04	0.04
query39	0.04	0.04	0.03
query40	0.18	0.17	0.16
query41	0.07	0.03	0.03
query42	0.04	0.03	0.03
query43	0.05	0.03	0.03
Total cold run time: 96.54 s
Total hot run time: 25.02 s

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