Skip to content

perf(arrow-buffer): add precomputed divisors for 32-bit and 64-bit integers - #11467

Open
mbutrovich wants to merge 18 commits into
apache:mainfrom
mbutrovich:divisor-types
Open

mbutrovich wants to merge 18 commits into
apache:mainfrom
mbutrovich:divisor-types

Conversation

@mbutrovich

@mbutrovich mbutrovich commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

This is the first divisor PR in #11421, so it settles the API that the i128 and i256 divisors and the kernel changes build on.

What changes are included in this PR?

  • DivisorU32, DivisorI32, DivisorU64 and DivisorI64 in arrow-buffer. No kernel uses them yet.
  • Each is a #[non_exhaustive] enum, and each variant is its own type whose methods don't branch on the divisor. A kernel matches on the variant once, outside the loop, and can fall back to wrapping_div_rem for variants it doesn't specialize. Matching on each value instead was 7-20% slower in Comet's bloom filter probe on four CPUs, as @mightsleep measured.
  • new picks the variant from the divisor and the target:
    • PowerOfTwo shifts and masks.
    • Up (u32, u64) uses the rounded-up multiplier when it is exact in N bits, so it needs no add and halve.
    • Down (u32 on aarch64 and on x86 with SSE4.1) uses the rounded-down multiplier with a saturating increment of the dividend, from Labor of Division (Episode III). Baseline x86-64 has no unsigned 32-bit min (pminud needs SSE4.1), so the saturating increment takes several instructions, and there its loop has more instructions than AddIndicator's.
    • AddIndicator uses figure 4.1 or 5.1 of Granlund and Montgomery for any other divisor.
  • The signed remainder comes from the quotient before the sign fix and |divisor|, because the remainder doesn't depend on the sign of the divisor (section 3.1 of Lemire, Kaser and Kurz).

Are these changes tested?

  • Unit tests and doctests in arrow-buffer/src/divisor.rs compare every type with the standard integer operators. They include both ends of the last quotient steps, every divisor of 2^N - 1, and the variant new picks for each divisor.
  • The divisor benchmark in arrow-buffer/benches/divisor.rs divides 8192 random values by 7, 13 and 16 with / and % and with the divisor, which reaches every variant.
  • The benchmark bot ran it on x86_64 (Xeon Platinum 8581C) at the default target and at x86-64-v3, and on arm64 (Neoverse-V2). The charts compare / and % with wrapping_div_rem in the changed column of each run. The divisor is faster in all 72 cases.
  • The gain is smallest on x86_64 for divisors that aren't powers of two: 2.4-4.9x for u64 and i64, whose high multiplies run in scalar registers at both targets, and 2.0-2.6x for i32 at the default target, because SSE2 has no signed 32-bit widening multiply (pmuldq needs SSE4.1).
divisor-final-bot-x86-v3 divisor-final-bot-x86 divisor-final-bot-arm

Are there any significant user-facing changes?

No existing API changes. The PR adds DivisorU32, DivisorU64, DivisorI32 and DivisorI64 to arrow-buffer. To divide a batch of values by one divisor, match on the variant once and call its wrapping_div_rem in the loop:

use arrow_buffer::DivisorU32;

let values: Vec<u32> = vec![100, 7, 64];

// `new` returns `None` for a divisor of 0
let quotients: Vec<u32> = match DivisorU32::new(7).unwrap() {
    DivisorU32::PowerOfTwo(d) => values.iter().map(move |n| d.wrapping_div_rem(*n).0).collect(),
    DivisorU32::AddIndicator(d) => values.iter().map(move |n| d.wrapping_div_rem(*n).0).collect(),
    // Up, Down, and any variant added later
    d => values.iter().map(move |n| d.wrapping_div_rem(*n).0).collect(),
};
assert_eq!(quotients, [14, 1, 9]);

The enums are #[non_exhaustive], so a match outside arrow-buffer needs the last arm. The rustdoc on each enum describes the variants.

AI Disclosure

I wrote this PR with help from Claude but understand and support the core approach, implementation, and test coverage.

@github-actions github-actions Bot added arrow Changes to the arrow crate arrow-buffer labels Oct 8, 2026
@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

baseline:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
changed:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing 7d3e750 (7d3e750) to 7d3e750 diff

Run configuration
run benchmark divisor
baseline:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
changed:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

baseline:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
changed:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
resources:
  arch: amd64

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing 7d3e750 (7d3e750) to 7d3e750 diff

Run configuration
run benchmark divisor
baseline:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
changed:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
resources:
  arch: amd64
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing 7d3e750 (7d3e750) to 7d3e750 diff

Run configuration
run benchmark divisor
baseline:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
changed:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
resources:
  arch: amd64
shards: 1
Details
group                  base                                     changed
-----                  ----                                     -------
i32/div 13/divisor     1.01      8.1±0.08µs 961.3 MElem/sec     1.00      8.0±0.08µs 975.4 MElem/sec
i32/div 13/operator    1.01     12.1±0.03µs 646.8 MElem/sec     1.00     12.0±0.10µs 650.8 MElem/sec
i32/div 16/divisor     1.00      6.1±0.06µs 1289.6 MElem/sec    1.01      6.1±0.06µs 1279.8 MElem/sec
i32/div 16/operator    1.00     12.0±0.09µs 650.6 MElem/sec     1.00     12.0±0.10µs 651.0 MElem/sec
i32/rem 13/divisor     1.01      8.5±0.02µs 916.6 MElem/sec     1.00      8.4±0.18µs 925.6 MElem/sec
i32/rem 13/operator    1.01     12.1±0.04µs 647.2 MElem/sec     1.00     11.9±0.12µs 654.3 MElem/sec
i32/rem 16/divisor     1.00      4.7±0.05µs 1656.4 MElem/sec    1.00      4.7±0.13µs 1653.6 MElem/sec
i32/rem 16/operator    1.00     12.0±0.11µs 653.7 MElem/sec     1.00     12.0±0.09µs 652.2 MElem/sec
i64/div 13/divisor     1.00      7.8±0.07µs 1006.2 MElem/sec    1.01      7.8±0.08µs 995.5 MElem/sec
i64/div 13/operator    1.00     19.9±0.18µs 392.2 MElem/sec     1.00     19.9±0.19µs 392.1 MElem/sec
i64/div 16/divisor     1.00      6.0±0.06µs 1295.5 MElem/sec    1.01      6.1±0.08µs 1283.8 MElem/sec
i64/div 16/operator    1.00     19.9±0.17µs 392.7 MElem/sec     1.00     20.0±0.16µs 391.5 MElem/sec
i64/rem 13/divisor     1.00      8.4±0.08µs 931.2 MElem/sec     1.01      8.5±0.08µs 922.6 MElem/sec
i64/rem 13/operator    1.00     19.9±0.18µs 392.6 MElem/sec     1.00     19.9±0.17µs 392.2 MElem/sec
i64/rem 16/divisor     1.00      4.7±0.03µs 1656.5 MElem/sec    1.01      4.7±0.04µs 1648.2 MElem/sec
i64/rem 16/operator    1.00     20.0±0.17µs 391.6 MElem/sec     1.00     19.9±0.16µs 391.6 MElem/sec
u32/div 13/divisor     1.01  1766.9±11.10ns  4.3 GElem/sec      1.00  1747.4±16.63ns  4.4 GElem/sec
u32/div 13/operator    1.00     12.0±0.12µs 653.4 MElem/sec     1.01     12.0±0.09µs 649.9 MElem/sec
u32/div 16/divisor     1.03    653.3±2.45ns 11.7 GElem/sec      1.00    636.9±5.66ns 12.0 GElem/sec
u32/div 16/operator    1.01     12.1±0.02µs 648.0 MElem/sec     1.00     11.9±0.12µs 655.1 MElem/sec
u32/rem 13/divisor     1.04      6.4±0.15µs 1211.4 MElem/sec    1.00      6.2±0.07µs 1264.7 MElem/sec
u32/rem 13/operator    1.01     12.1±0.03µs 648.2 MElem/sec     1.00     12.0±0.12µs 652.6 MElem/sec
u32/rem 16/divisor     1.02      3.4±0.07µs  2.2 GElem/sec      1.00      3.3±0.03µs  2.3 GElem/sec
u32/rem 16/operator    1.01     12.1±0.02µs 648.1 MElem/sec     1.00     11.9±0.11µs 654.6 MElem/sec
u64/div 13/divisor     1.01      5.0±0.08µs 1563.7 MElem/sec    1.00      5.0±0.05µs 1573.5 MElem/sec
u64/div 13/operator    1.01     20.0±0.13µs 390.4 MElem/sec     1.00     19.8±0.18µs 394.0 MElem/sec
u64/div 16/divisor     1.02  1275.7±29.09ns  6.0 GElem/sec      1.00  1256.5±17.41ns  6.1 GElem/sec
u64/div 16/operator    1.00     19.9±0.18µs 392.9 MElem/sec     1.00     19.9±0.19µs 392.6 MElem/sec
u64/rem 13/divisor     1.00      6.0±0.08µs 1296.2 MElem/sec    1.01      6.1±0.12µs 1289.7 MElem/sec
u64/rem 13/operator    1.00     20.0±0.16µs 391.4 MElem/sec     1.00     19.9±0.18µs 392.4 MElem/sec
u64/rem 16/divisor     1.00      3.1±0.04µs  2.5 GElem/sec      1.02      3.1±0.05µs  2.4 GElem/sec
u64/rem 16/operator    1.00     19.9±0.20µs 391.9 MElem/sec     1.00     19.9±0.15µs 391.9 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-im36g35r-ba30b79c-kdgh

Instance: c4-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6070532604-3218-8nzhr 6.12.94+ #1 SMP Fri Aug 21 07:56:42 UTC 2026 x86_64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            x86_64
CPU op-mode(s):                          32-bit, 64-bit
Address sizes:                           52 bits physical, 57 bits virtual
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               GenuineIntel
Model name:                              INTEL(R) XEON(R) PLATINUM 8581C CPU @ 2.30GHz
CPU family:                              6
Model:                                   207
Thread(s) per core:                      2
Core(s) per socket:                      8
Socket(s):                               1
Stepping:                                2
BogoMIPS:                                4600.00
Flags:                                   fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq monitor ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves avx_vnni avx512_bf16 wbnoinvd arat avx512vbmi umip avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid cldemote movdiri movdir64b fsrm md_clear serialize tsxldtrk amx_bf16 avx512_fp16 amx_tile amx_int8 arch_capabilities
Hypervisor vendor:                       KVM
Virtualization type:                     full
L1d cache:                               384 KiB (8 instances)
L1i cache:                               256 KiB (8 instances)
L2 cache:                                16 MiB (8 instances)
L3 cache:                                260 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; Enhanced / Automatic IBRS; IBPB conditional; PBRSB-eIBRS SW sequence; BHI BHI_DIS_S
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

base (merge-base)

Metric Value
Wall time 305.1s
Peak memory 11.0 MiB
Avg memory 10.4 MiB
CPU user 300.3s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 305.1s
Peak memory 10.9 MiB
Avg memory 10.4 MiB
CPU user 299.3s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing 7d3e750 (7d3e750) to 7d3e750 diff

Run configuration
run benchmark divisor
baseline:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
changed:
  ref: 7d3e750bacf5ab46e465b2241597a86de054050f
shards: 1
Details
group                  base                                     changed
-----                  ----                                     -------
i32/div 13/divisor     1.02      9.0±0.00µs 864.9 MElem/sec     1.00      8.8±0.02µs 884.4 MElem/sec
i32/div 13/operator    1.00     30.0±0.01µs 260.3 MElem/sec     1.00     30.0±0.01µs 260.3 MElem/sec
i32/div 16/divisor     1.00      6.2±0.01µs 1251.9 MElem/sec    1.00      6.2±0.02µs 1255.4 MElem/sec
i32/div 16/operator    1.00     14.3±0.07µs 547.5 MElem/sec     1.00     14.3±0.03µs 547.4 MElem/sec
i32/rem 13/divisor     1.00     10.0±0.02µs 782.1 MElem/sec     1.00     10.0±0.01µs 781.3 MElem/sec
i32/rem 13/operator    1.00     32.8±0.01µs 238.5 MElem/sec     1.00     32.8±0.01µs 238.4 MElem/sec
i32/rem 16/divisor     1.00      6.5±0.00µs 1201.9 MElem/sec    1.00      6.5±0.01µs 1200.0 MElem/sec
i32/rem 16/operator    1.00     16.5±0.00µs 474.3 MElem/sec     1.00     16.5±0.01µs 474.0 MElem/sec
i64/div 13/divisor     1.00      9.0±0.01µs 871.3 MElem/sec     1.00      9.0±0.00µs 867.7 MElem/sec
i64/div 13/operator    1.00     52.0±0.01µs 150.3 MElem/sec     1.00     52.0±0.02µs 150.3 MElem/sec
i64/div 16/divisor     1.00      6.2±0.00µs 1250.3 MElem/sec    1.00      6.2±0.00µs 1250.5 MElem/sec
i64/div 16/operator    1.02     14.1±0.18µs 554.7 MElem/sec     1.00     13.8±0.14µs 564.5 MElem/sec
i64/rem 13/divisor     1.01      9.8±0.01µs 796.2 MElem/sec     1.00      9.7±0.00µs 806.6 MElem/sec
i64/rem 13/operator    1.00     54.7±0.03µs 142.7 MElem/sec     1.00     54.7±0.01µs 142.8 MElem/sec
i64/rem 16/divisor     1.00      6.5±0.01µs 1198.3 MElem/sec    1.00      6.5±0.00µs 1199.5 MElem/sec
i64/rem 16/operator    1.00     16.5±0.01µs 473.8 MElem/sec     1.00     16.5±0.01µs 474.0 MElem/sec
u32/div 13/divisor     1.00   1687.3±1.21ns  4.5 GElem/sec      1.00   1687.3±0.52ns  4.5 GElem/sec
u32/div 13/operator    1.00     31.5±0.02µs 248.0 MElem/sec     1.00     31.5±0.01µs 248.0 MElem/sec
u32/div 16/divisor     1.00    408.3±1.05ns 18.7 GElem/sec      1.00    407.7±0.93ns 18.7 GElem/sec
u32/div 16/operator    1.00     14.2±0.01µs 549.6 MElem/sec     1.00     14.2±0.02µs 550.3 MElem/sec
u32/rem 13/divisor     1.00      6.0±0.00µs 1310.7 MElem/sec    1.00      6.0±0.01µs 1311.4 MElem/sec
u32/rem 13/operator    1.00     34.2±0.01µs 228.2 MElem/sec     1.00     34.2±0.01µs 228.2 MElem/sec
u32/rem 16/divisor     1.00      5.5±0.05µs 1412.2 MElem/sec    1.00      5.5±0.00µs 1411.8 MElem/sec
u32/rem 16/operator    1.00     16.5±0.03µs 473.1 MElem/sec     1.00     16.5±0.01µs 474.1 MElem/sec
u64/div 13/divisor     1.02      5.2±0.00µs 1515.8 MElem/sec    1.00      5.1±0.02µs 1541.4 MElem/sec
u64/div 13/operator    1.00     53.4±0.01µs 146.2 MElem/sec     1.00     53.4±0.02µs 146.2 MElem/sec
u64/div 16/divisor     1.00    799.4±3.74ns  9.5 GElem/sec      1.00    799.3±3.26ns  9.5 GElem/sec
u64/div 16/operator    1.01     15.0±0.04µs 519.1 MElem/sec     1.00     15.0±0.06µs 522.2 MElem/sec
u64/rem 13/divisor     1.00      5.5±0.01µs 1410.1 MElem/sec    1.00      5.5±0.01µs 1409.0 MElem/sec
u64/rem 13/operator    1.00     56.2±0.03µs 139.1 MElem/sec     1.00     56.2±0.02µs 139.0 MElem/sec
u64/rem 16/divisor     1.00      5.5±0.01µs 1415.5 MElem/sec    1.00      5.5±0.00µs 1415.7 MElem/sec
u64/rem 16/operator    1.00     16.5±0.01µs 474.2 MElem/sec     1.00     16.5±0.01µs 473.9 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-1sqhi6ce-a70ada09-95cm

Instance: c4a-highcpu-48 (12 vCPU / 65 GiB)

uname:

Linux bench-c6070529477-3217-t9h65 6.12.94+ #1 SMP Fri Aug 21 08:00:16 UTC 2026 aarch64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  48
On-line CPU(s) list:                     0-47
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     48
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               3 MiB (48 instances)
L1i cache:                               3 MiB (48 instances)
L2 cache:                                96 MiB (48 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-47
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

base (merge-base)

Metric Value
Wall time 305.1s
Peak memory 10.1 MiB
Avg memory 9.3 MiB
CPU user 301.1s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 305.1s
Peak memory 10.1 MiB
Avg memory 9.6 MiB
CPU user 300.4s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@mbutrovich

This comment was marked as outdated.

@andygrove andygrove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I relied on AI to do this review because I am not an expert in this area. Feel free to push back (as Claude would say) if you disagree with any of this!


Thanks @mbutrovich, this is a careful PR. I checked the math beyond the unit tests, and it holds. I compared div_rem and checked_div_rem against wrapping_* and checked_* for:

  • every u32 dividend with 77 divisors, including every 2^k ± 1, 10^k, MAX and 2^31 + 1
  • every i32 dividend with 21 divisors, including MIN, MAX and ±1
  • every u32 and i32 divisor, with boundary dividends such as the largest multiple of d and the value just below it
  • about 100M random u64 and i64 divisors

There were no mismatches.

My main concern is the premise that LLVM moves the power_of_two branch out of the loop. Since this PR settles the API, it's the right time to raise it.

1. The power_of_two branch is often not hoisted, so performance depends on build settings

LLVM hoists the test only when the loop ends up inlined somewhere it can prove the output stores don't alias the divisor (divisor.rs:80). When it can't, the loop reloads power_of_two, the multiplier and the shift from memory for every element. It branches per element and isn't vectorized. I confirmed this in the disassembly (ldrb [x11, #0x1c]; tbnz inside an out-of-line Vec::from_iter instance).

The benchmark bot forces CARGO_PROFILE_BENCH_LTO=thin (build_env.rs). Building with that setting on an M3 Ultra with rustc 1.99.0, 6 of the 8 divisor loops compile to this scalar, branch-per-element form. Here is the PR's own benchmark, in µs per 8192 values:

Case / or % div_rem, default cargo bench div_rem, thin LTO (bot setting) div_rem, fat LTO with 1 codegen unit
u32 % 13 4.19 0.96 4.56 0.93
u32 % 16 4.18 0.43 2.45 0.58
i32 / 13 4.90 1.48 6.08 1.43
i32 / 16 4.40 0.83 4.41 0.80
i64 / 13 4.41 3.12 5.72 3.09
i64 % 16 4.33 4.02 4.09 1.44

Even the default cargo bench has the problem for i64 %. I got the same result in an aarch64 Linux container. This is probably also what the bot results posted here show, such as u32 % 16 taking 5.5 µs against 0.41 µs for u32 / 16 on Neoverse-V2. A mask isn't 13x slower than a shift unless the loop isn't vectorized. If so, those numbers mostly reflect this codegen behavior rather than the algorithm, so I'd hold off on choosing kernels per type from them.

The same thing happens through PrimitiveArray::unary, which is how the kernels will call this. In a default release build, Int64Array % 16 with unary(|x| d.div_rem(x).1) takes 4.05 µs, against 4.16 µs for wrapping_rem. Testing power_of_two once outside the loop and passing a branch-free closure to unary takes 1.40 µs. With thin LTO, dispatching outside the loop gives:

  • u32 % 13: 5.00 → 0.96 µs
  • u64 % 16: 3.06 → 0.91 µs
  • i64 % 16: 4.02 → 1.52 µs

It does not fix the signed multiply path: i32 / 13 goes from 6.41 to 5.43 µs, still slower than wrapping_div at 4.28 µs. unary's map(|v| op(*v)) reaches the closure state through a pointer, and once from_iter isn't inlined, LLVM can't hoist those loads. A plain loop writing into a preallocated output, with the divisor in a local, was fast in every case in both the default and thin-LTO builds (for example 1.43 µs for i32 / 13 and 1.27 µs for i64 % 16).

Suggestions:

  • Make the shift-or-multiply choice something callers can dispatch on once outside the loop, instead of relying on loop unswitching. For example, add a method that returns an enum of two branch-free divisor types. The epic already plans to match on the divisor's variant once outside the loop for the width variants; the power-of-two choice needs the same treatment. Today that isn't possible, because power_of_two is private and div_rem always branches.
  • Reword the comment at divisor.rs:80 and the corresponding sentence in the PR description.
  • Have the benchmark call the divisor through unary, as the kernels will, and check that the inner loop is vectorized in the bot's thin-LTO build before using its numbers.

2. The signed types could use figure 5.2 of the same paper (optional, API unchanged)

Figure 5.2 divides with a signed multiplier and smulh, with no abs and one sign fix instead of two. I prototyped it and checked it against wrapping_div and wrapping_rem: exhaustively for i32 with 14 divisors, and on 80M random i64 pairs. Timings are for a loop over a preallocated output on the same M3, in µs per 8192 values:

Case / or % This PR Figure 5.2
i64 / 13 4.21 2.92 2.22
i64 % 13 4.04 5.57 3.38
i32 / 13 1.40 1.02
i32 % 13 1.40 1.15

This turns i64 % 13, the one case where the divisor was slower than %, into a speedup. It would also remove the signed types' dependence on the unsigned ones. Powers of two would still want their own shift path.

Prototype
macro_rules! gm52 {
    ($name:ident, $t:ty, $u:ty, $wide:ty) => {
        #[derive(Clone, Copy)]
        pub struct $name { m: $t, d: $t, sh_post: u32, d_sign: $t }
        impl $name {
            pub fn new(d: $t) -> Self {
                let ad = d.unsigned_abs();
                let l = (<$u>::BITS - (ad - 1).leading_zeros()).max(1);
                // m = 1 + floor(2^(N+l-1) / |d|), m' = m - 2^N (fits in the signed type)
                let m = 1 + ((1 as $wide) << (<$t>::BITS + l - 1)) / ad as $wide;
                let m_prime = m.wrapping_sub((1 as $wide) << <$t>::BITS) as $t;
                Self { m: m_prime, d, sh_post: l - 1, d_sign: d >> (<$t>::BITS - 1) }
            }
            #[inline]
            pub fn wrapping_div_rem(&self, n: $t) -> ($t, $t) {
                let mulsh = ((self.m as $wide * n as $wide) >> <$t>::BITS) as $t;
                let q0 = n.wrapping_add(mulsh);
                let q0 = (q0 >> self.sh_post).wrapping_sub(n >> (<$t>::BITS - 1));
                let q = (q0 ^ self.d_sign).wrapping_sub(self.d_sign);
                (q, n.wrapping_sub(q.wrapping_mul(self.d)))
            }
        }
    };
}
gm52!(Gm52I32, i32, u32, i64);
gm52!(Gm52I64, i64, u64, i128);

3. Naming

i256::wrapping_div_rem and i256::checked_div_rem (#11425, same epic) use the wrapping_ prefix. Here the wrapping version on the signed types is div_rem. The sealed trait will cover both, so it would help to pick one convention now: either wrapping_div_rem here, or rename the i256 methods before a release. A checked_div_rem on the unsigned types that always returns Some would also give the trait the same set of methods on every type.

Smaller things

  • new could be a const fn; in the signed new, a match would replace ?. That would match i256's const constructors and allow compile-time tables of 10^k divisors, which the epic mentions.
  • checked_div_rem recomputes minus_one on every call (divisor.rs:164). It could be a field set in new, which matters when the loop isn't hoisted as described in point 1.
  • The benchmark groups are named just u32, i32 and so on. Prefixing them with divisor would make them easier to identify in bot output.
  • #[doc = concat!("the quotient truncates toward zero …")] concatenates a single literal, so it could be a plain /// line.

@mbutrovich

Copy link
Copy Markdown
Contributor Author

Thanks @andygrove. I made the API change you suggested and adopted figure 5.2, and I updated the PR description to match the new API and numbers. The numbers below are from an Apple M5 Max with rustc 1.99.0, in us per 8192 values, from the PR's divisor benchmark unless I say otherwise.

Hoisting the power-of-two branch

My main concern is the premise that LLVM moves the power_of_two branch out of the loop.

You were right, and your thin LTO numbers reproduce here. I found two separate problems.

The first is how the loop reaches the divisor. The benchmark's closure captured the divisor by reference, which fits the per-element loads you saw in the disassembly. Moving a copy of the divisor into the closure (move |x| d.div_rem(*x)) fixed every case in both the default and the thin LTO build. Through unary, a move closure at the call site didn't help. unary wraps the closure in map(|v| op(*v)), which captures op by reference, and changing only that line to map(move |v| op(*v)) fixed it. I'll open a separate PR for that change, since it affects every unary caller that captures state.

The second is that move alone wasn't enough at every optimization level. At opt-level=2, the power-of-two path gave no gain even with move: u32 % 16 took 1.11 us, the same as u32 % 13. So the API now does what you suggested. new returns an enum with a PowerOfTwo variant and a Multiplier variant, and each variant's methods are branch-free. A kernel matches on the variant once, outside the loop, and moves the variant into its closure. The enum also has wrapping_div_rem, which matches on each call, for callers that divide a few values. The comment at divisor.rs:80 and the sentence in the PR description went away with the branch.

With thin LTO, your cases now look like this:

Case / or % Before After
u32 % 13 3.73 4.05 0.86
u32 % 16 3.77 2.03 0.30
i32 / 13 3.64 5.14 0.97
i32 / 16 3.66 3.27 0.73
i64 / 13 3.70 4.94 2.16
i64 % 16 3.79 3.04 1.31

At opt-level=2, the power-of-two cases with the old API and a move closure, against the new API:

Case / or % Old API with move New API
u32 % 16 3.91 1.11 0.31
i32 / 16 4.23 1.47 0.73
u64 % 16 4.06 2.04 0.65
i64 % 16 3.87 2.69 1.34
All cases with the new API
Case / or %, default Divisor, default / or %, thin LTO Divisor, thin LTO / or %, opt-level=2 Divisor, opt-level=2
u32 / 13 3.63 0.73 3.65 0.73 3.62 0.74
u32 % 13 3.66 0.86 3.73 0.86 3.86 0.87
u32 / 16 3.64 0.28 3.63 0.29 3.62 0.30
u32 % 16 3.69 0.28 3.77 0.30 3.91 0.31
i32 / 13 3.63 0.97 3.64 0.97 4.12 0.98
i32 % 13 3.68 1.09 3.71 1.09 3.96 1.11
i32 / 16 3.63 0.73 3.66 0.73 4.23 0.73
i32 % 16 3.70 0.62 3.71 0.62 3.75 0.62
u64 / 13 3.68 1.82 3.70 1.84 3.69 1.73
u64 % 13 3.75 2.83 3.81 2.94 4.09 2.87
u64 / 16 3.73 0.64 3.71 0.66 3.69 0.63
u64 % 16 3.78 0.64 3.81 0.65 4.06 0.65
i64 / 13 3.69 2.13 3.70 2.16 3.90 2.16
i64 % 13 3.77 3.32 3.76 3.59 4.02 3.27
i64 / 16 3.69 1.53 3.70 1.53 3.84 1.54
i64 % 16 3.80 1.31 3.79 1.31 3.87 1.34

Have the benchmark call the divisor through unary, as the kernels will, and check that the inner loop is vectorized in the bot's thin-LTO build before using its numbers.

arrow-buffer can't depend on arrow-array, so the benchmark can't call unary. It now does what a kernel will do: it matches on the variant once and maps the values with a move closure. The kernel PR will measure the unary path with the integer_division benchmark from #11427. I agree about the earlier bot runs. They measured the by-reference loop, so I'll rerun the bot on the new commit before anything uses those numbers.

Figure 5.2 for the signed types

Figure 5.2 divides with a signed multiplier and smulh, with no abs and one sign fix instead of two.

Thanks for prototyping this. The signed Multiplier variant now uses figure 5.2 of Granlund and Montgomery (1994), and the signed PowerOfTwo variant shifts with a bias of |d| - 1 for negative dividends, so the signed types no longer use the unsigned ones. i64 % 13 went from 5.11 to 3.32 us in the default build, against 3.77 for %, so on this machine the divisor is now faster than / and % in all 32 cases in the default, thin LTO and opt-level=2 builds. i32 / 13 went from 1.32 to 0.97. Besides the unit tests, I compared both signed and unsigned results against wrapping_div, wrapping_rem, checked_div and checked_rem for every 32-bit dividend with 70 divisors, and for 200M random 64-bit pairs, with no mismatches.

Naming

Here the wrapping version on the signed types is div_rem. The sealed trait will cover both, so it would help to pick one convention now

All four types now use wrapping_div_rem, to match i256.

A checked_div_rem on the unsigned types that always returns Some would also give the trait the same set of methods on every type.

I'd like to keep checked_div_rem off the types where it can't fail, for two reasons.

The first is consistency with the rest of arrow-rs. The inherent checked_* methods on i256 and the interval types can all return None. Methods that can't fail exist only where a trait needs the same set of methods on every type, such as ArrowNativeTypeOp::add_checked for floats, which returns Ok(self + rhs). The sealed trait in #11421 is where the uniform set belongs, and it can implement checked_div_rem for the unsigned types the same way.

The second is performance in the kernels. When the divisor can't fail, a kernel can use unary instead of try_unary, and the difference is large. I timed a Multiplier divisor by 13 with unary and wrapping_div_rem, against try_unary and a closure that returns Some(d.wrapping_div_rem(x).0), which is what an always-Some checked_div_rem inlines to:

Case unary, no nulls try_unary, no nulls unary, every fifth value null try_unary, every fifth value null
u32 / 13 0.72 0.88 0.72 4.35
i32 / 13 0.95 1.14 0.95 4.51
u64 / 13 1.68 2.36 1.70 4.86

With nulls, try_unary visits only the valid slots, one index at a time, while unary computes every slot. A divisor type without a checked_div_rem that can't fail steers kernels to unary.

The same reasoning applies to the signed Multiplier variant. Its divisor has a magnitude of at least 3, so its quotient can't overflow, and it has only wrapping_div_rem. Only the signed PowerOfTwo variant, which holds -1, and the signed enum have checked_div_rem. A kernel can test for -1 once, outside the loop, and use unary for every other divisor.

Smaller points

new could be a const fn

new is a const fn now, and so are wrapping_div_rem and checked_div_rem. A test builds const divisors.

checked_div_rem recomputes minus_one on every call (divisor.rs:164).

Only the signed PowerOfTwo variant checks for -1 now. It compares n with MIN and two of its fields, and the kernel can skip it for any divisor other than -1.

The benchmark groups are named just u32, i32 and so on.

They're divisor_u32, divisor_i32, divisor_u64 and divisor_i64 now.

concatenates a single literal, so it could be a plain /// line.

That line went away when I rewrote the signed docs.

@mbutrovich
mbutrovich requested a review from andygrove October 9, 2026 17:03
@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

baseline:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
changed:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing 63ae153 (63ae153) to 63ae153 diff

Run configuration
run benchmark divisor
baseline:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
changed:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

baseline:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
changed:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
resources:
  arch: amd64

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing 63ae153 (63ae153) to 63ae153 diff

Run configuration
run benchmark divisor
baseline:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
changed:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
resources:
  arch: amd64
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing 63ae153 (63ae153) to 63ae153 diff

Run configuration
run benchmark divisor
baseline:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
changed:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
resources:
  arch: amd64
shards: 1
Details
group                          base                                     changed
-----                          ----                                     -------
divisor_i32/div 13/divisor     1.01      5.9±0.12µs 1333.1 MElem/sec    1.00      5.8±0.06µs 1346.2 MElem/sec
divisor_i32/div 13/operator    1.00     15.1±0.18µs 517.6 MElem/sec     1.00     15.1±0.21µs 516.4 MElem/sec
divisor_i32/div 16/divisor     1.01  1496.0±14.17ns  5.1 GElem/sec      1.00  1485.3±20.20ns  5.1 GElem/sec
divisor_i32/div 16/operator    1.00     15.1±0.14µs 517.9 MElem/sec     1.00     15.1±0.18µs 518.2 MElem/sec
divisor_i32/rem 13/divisor     1.00      7.7±0.12µs 1014.8 MElem/sec    1.00      7.7±0.15µs 1016.0 MElem/sec
divisor_i32/rem 13/operator    1.00     15.1±0.18µs 516.5 MElem/sec     1.00     15.1±0.22µs 516.7 MElem/sec
divisor_i32/rem 16/divisor     1.00  1111.8±11.38ns  6.9 GElem/sec      1.02  1129.0±49.51ns  6.8 GElem/sec
divisor_i32/rem 16/operator    1.00     15.1±0.20µs 516.1 MElem/sec     1.00     15.1±0.35µs 518.1 MElem/sec
divisor_i64/div 13/divisor     1.02      9.7±0.15µs 802.0 MElem/sec     1.00      9.6±0.23µs 817.7 MElem/sec
divisor_i64/div 13/operator    1.02     25.2±0.25µs 310.5 MElem/sec     1.00     24.7±0.63µs 316.0 MElem/sec
divisor_i64/div 16/divisor     1.02      4.3±0.06µs 1801.3 MElem/sec    1.00      4.2±0.12µs 1839.4 MElem/sec
divisor_i64/div 16/operator    1.02     25.2±0.29µs 310.0 MElem/sec     1.00     24.7±0.59µs 316.6 MElem/sec
divisor_i64/rem 13/divisor     1.02     13.9±0.19µs 563.7 MElem/sec     1.00     13.6±0.26µs 574.3 MElem/sec
divisor_i64/rem 13/operator    1.02     25.2±0.28µs 310.5 MElem/sec     1.00     24.7±0.44µs 315.9 MElem/sec
divisor_i64/rem 16/divisor     1.02      2.6±0.03µs  2.9 GElem/sec      1.00      2.6±0.06µs  3.0 GElem/sec
divisor_i64/rem 16/operator    1.01     25.2±0.31µs 310.0 MElem/sec     1.00     25.0±0.63µs 313.0 MElem/sec
divisor_u32/div 13/divisor     1.01      2.2±0.04µs  3.4 GElem/sec      1.00      2.2±0.03µs  3.5 GElem/sec
divisor_u32/div 13/operator    1.00     15.1±0.18µs 517.0 MElem/sec     1.00     15.1±0.21µs 516.5 MElem/sec
divisor_u32/div 16/divisor     1.00   783.4±10.93ns  9.7 GElem/sec      1.12    874.1±6.93ns  8.7 GElem/sec
divisor_u32/div 16/operator    1.00     15.2±0.24µs 514.2 MElem/sec     1.00     15.1±0.16µs 516.1 MElem/sec
divisor_u32/rem 13/divisor     1.00      4.0±0.05µs 1943.4 MElem/sec    1.00      4.0±0.07µs 1939.1 MElem/sec
divisor_u32/rem 13/operator    1.00     15.1±0.16µs 517.8 MElem/sec     1.00     15.1±0.19µs 518.4 MElem/sec
divisor_u32/rem 16/divisor     1.03   760.7±11.44ns 10.0 GElem/sec      1.00    742.1±8.88ns 10.3 GElem/sec
divisor_u32/rem 16/operator    1.00     15.1±0.14µs 518.2 MElem/sec     1.00     15.1±0.20µs 517.0 MElem/sec
divisor_u64/div 13/divisor     1.01      6.2±0.16µs 1261.2 MElem/sec    1.00      6.2±0.10µs 1268.7 MElem/sec
divisor_u64/div 13/operator    1.01     25.3±0.41µs 308.7 MElem/sec     1.00     25.1±0.21µs 311.3 MElem/sec
divisor_u64/div 16/divisor     1.02  1536.3±28.48ns  5.0 GElem/sec      1.00  1511.5±52.91ns  5.0 GElem/sec
divisor_u64/div 16/operator    1.02     25.2±0.34µs 310.1 MElem/sec     1.00     24.6±0.44µs 317.3 MElem/sec
divisor_u64/rem 13/divisor     1.01      9.7±0.29µs 803.8 MElem/sec     1.00      9.6±0.19µs 811.7 MElem/sec
divisor_u64/rem 13/operator    1.02     25.2±0.36µs 309.5 MElem/sec     1.00     24.8±0.57µs 314.9 MElem/sec
divisor_u64/rem 16/divisor     1.02  1473.3±21.81ns  5.2 GElem/sec      1.00  1451.4±27.91ns  5.3 GElem/sec
divisor_u64/rem 16/operator    1.01     25.1±0.25µs 311.0 MElem/sec     1.00     24.8±0.45µs 314.6 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-3lld71t1-69c7ab42-nfkx

Instance: c4-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6085591777-3250-sg7jw 6.12.94+ #1 SMP Fri Aug 21 07:56:42 UTC 2026 x86_64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            x86_64
CPU op-mode(s):                          32-bit, 64-bit
Address sizes:                           52 bits physical, 57 bits virtual
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               GenuineIntel
Model name:                              INTEL(R) XEON(R) PLATINUM 8581C CPU @ 2.30GHz
CPU family:                              6
Model:                                   207
Thread(s) per core:                      2
Core(s) per socket:                      8
Socket(s):                               1
Stepping:                                2
BogoMIPS:                                4600.00
Flags:                                   fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq monitor ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves avx_vnni avx512_bf16 wbnoinvd arat avx512vbmi umip avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid cldemote movdiri movdir64b fsrm md_clear serialize tsxldtrk amx_bf16 avx512_fp16 amx_tile amx_int8 arch_capabilities
Hypervisor vendor:                       KVM
Virtualization type:                     full
L1d cache:                               384 KiB (8 instances)
L1i cache:                               256 KiB (8 instances)
L2 cache:                                16 MiB (8 instances)
L3 cache:                                260 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; Enhanced / Automatic IBRS; IBPB conditional; PBRSB-eIBRS SW sequence; BHI BHI_DIS_S
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

base (merge-base)

Metric Value
Wall time 295.1s
Peak memory 10.9 MiB
Avg memory 10.1 MiB
CPU user 290.0s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 295.1s
Peak memory 11.1 MiB
Avg memory 10.3 MiB
CPU user 288.3s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing 63ae153 (63ae153) to 63ae153 diff

Run configuration
run benchmark divisor
baseline:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
changed:
  ref: 63ae1539bf03bbea3302c06094984320ba89160b
shards: 1
Details
group                          base                                     changed
-----                          ----                                     -------
divisor_i32/div 13/divisor     1.00      2.1±0.00µs  3.6 GElem/sec      1.00      2.1±0.00µs  3.6 GElem/sec
divisor_i32/div 13/operator    1.00     30.0±0.01µs 260.4 MElem/sec     1.00     30.0±0.04µs 260.2 MElem/sec
divisor_i32/div 16/divisor     1.00   1320.2±0.42ns  5.8 GElem/sec      1.00   1320.2±0.44ns  5.8 GElem/sec
divisor_i32/div 16/operator    1.00     13.7±0.00µs 569.2 MElem/sec     1.00     13.7±0.00µs 569.1 MElem/sec
divisor_i32/rem 13/divisor     1.00      2.4±0.00µs  3.2 GElem/sec      1.00      2.4±0.00µs  3.2 GElem/sec
divisor_i32/rem 13/operator    1.00     30.7±0.01µs 254.5 MElem/sec     1.00     30.7±0.03µs 254.4 MElem/sec
divisor_i32/rem 16/divisor     1.00   1101.7±0.47ns  6.9 GElem/sec      1.00   1101.6±0.40ns  6.9 GElem/sec
divisor_i32/rem 16/operator    1.00     14.4±0.00µs 541.9 MElem/sec     1.00     14.4±0.00µs 541.8 MElem/sec
divisor_i64/div 13/divisor     1.00      6.0±0.00µs 1306.2 MElem/sec    1.00      6.0±0.00µs 1306.5 MElem/sec
divisor_i64/div 13/operator    1.00     52.0±0.01µs 150.3 MElem/sec     1.00     52.0±0.01µs 150.3 MElem/sec
divisor_i64/div 16/divisor     1.00      2.6±0.00µs  2.9 GElem/sec      1.00      2.6±0.00µs  2.9 GElem/sec
divisor_i64/div 16/operator    1.00     13.7±0.00µs 568.9 MElem/sec     1.00     13.7±0.00µs 568.9 MElem/sec
divisor_i64/rem 13/divisor     1.00      6.1±0.04µs 1273.8 MElem/sec    1.07      6.5±0.33µs 1194.3 MElem/sec
divisor_i64/rem 13/operator    1.00     54.7±0.02µs 142.8 MElem/sec     1.00     54.7±0.02µs 142.8 MElem/sec
divisor_i64/rem 16/divisor     1.00      2.2±0.00µs  3.5 GElem/sec      1.00      2.2±0.00µs  3.5 GElem/sec
divisor_i64/rem 16/operator    1.00     16.5±0.01µs 474.1 MElem/sec     1.00     16.5±0.01µs 474.1 MElem/sec
divisor_u32/div 13/divisor     1.00   1686.4±0.75ns  4.5 GElem/sec      1.00   1686.2±0.67ns  4.5 GElem/sec
divisor_u32/div 13/operator    1.00     31.5±0.01µs 248.1 MElem/sec     1.00     31.5±0.01µs 248.1 MElem/sec
divisor_u32/div 16/divisor     1.00    412.0±3.38ns 18.5 GElem/sec      1.00    411.5±4.01ns 18.5 GElem/sec
divisor_u32/div 16/operator    1.00     13.7±0.00µs 569.2 MElem/sec     1.00     13.7±0.00µs 569.2 MElem/sec
divisor_u32/rem 13/divisor     1.00   1767.3±0.61ns  4.3 GElem/sec      1.00   1767.4±0.57ns  4.3 GElem/sec
divisor_u32/rem 13/operator    1.00     32.2±0.01µs 242.7 MElem/sec     1.00     32.2±0.01µs 242.8 MElem/sec
divisor_u32/rem 16/divisor     1.00    406.6±0.14ns 18.8 GElem/sec      1.00    407.6±0.76ns 18.7 GElem/sec
divisor_u32/rem 16/operator    1.00     14.4±0.00µs 541.9 MElem/sec     1.00     14.4±0.01µs 541.8 MElem/sec
divisor_u64/div 13/divisor     1.00      5.2±0.00µs 1515.7 MElem/sec    1.01      5.2±0.00µs 1507.2 MElem/sec
divisor_u64/div 13/operator    1.01     53.7±0.04µs 145.5 MElem/sec     1.00     53.4±0.02µs 146.2 MElem/sec
divisor_u64/div 16/divisor     1.00    796.7±0.95ns  9.6 GElem/sec      1.00    797.6±0.75ns  9.6 GElem/sec
divisor_u64/div 16/operator    1.00     13.7±0.00µs 568.9 MElem/sec     1.00     13.7±0.00µs 568.9 MElem/sec
divisor_u64/rem 13/divisor     1.00      5.3±0.01µs 1484.0 MElem/sec    1.00      5.2±0.01µs 1488.5 MElem/sec
divisor_u64/rem 13/operator    1.00     56.2±0.02µs 139.1 MElem/sec     1.00     56.2±0.01µs 139.1 MElem/sec
divisor_u64/rem 16/divisor     1.00    796.8±1.10ns  9.6 GElem/sec      1.00    796.4±0.97ns  9.6 GElem/sec
divisor_u64/rem 16/operator    1.00     16.5±0.00µs 474.1 MElem/sec     1.00     16.5±0.01µs 474.0 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-1konwh4m-230e413b-55zz

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6085588740-3249-jvn8v 6.12.94+ #1 SMP Fri Aug 21 08:00:16 UTC 2026 aarch64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

base (merge-base)

Metric Value
Wall time 300.1s
Peak memory 10.1 MiB
Avg memory 9.5 MiB
CPU user 297.1s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 300.1s
Peak memory 10.1 MiB
Avg memory 9.5 MiB
CPU user 296.4s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@mbutrovich

Copy link
Copy Markdown
Contributor Author

The benchmark bot reran the divisor benchmark on 63ae153, which has the same benchmark code as the PR head, on x86 and arm64. Each run compares the commit to itself, so the charts show operator time divided by divisor time from the changed column.

divisor-types-enum-bot-x86 divisor-types-enum-bot-arm

wrapping_div_rem is faster than / and % in all 32 cases on both runners. The smallest gain is 1.8x, for i64 % 13 on x86.

@andygrove, in your review you asked to

check that the inner loop is vectorized in the bot's thin-LTO build before using its numbers.

I built the divisor benchmark with the bot's setting (CARGO_PROFILE_BENCH_LTO=thin) for aarch64 and for x86-64 without a target-cpu, and read the disassembly of the benchmark binary. Each benchmark matches on the variant once per batch, outside the loop. None of the inner loops branches on the divisor or reloads its fields. On aarch64, the 32-bit loops and the 64-bit PowerOfTwo loops are NEON loops. The 64-bit Multiplier division loops compute umulh or smulh in scalar registers for two values per iteration and do the rest in NEON registers, and the 64-bit Multiplier remainder loops are scalar. On x86-64, every case has an SSE2 main loop, and the 64-bit Multiplier loops compute the high multiplies with scalar mul for two values per iteration.

On Neoverse-V2, u32 % 16 now takes 0.41 us, the same as u32 / 16. In the earlier run it took 5.5 us. / and % by 16 take 13.7 to 16.5 us there, against 30.0 to 56.2 us by 13, so three of the divide-by-16 bars are shorter than the divide-by-13 bars.

Numbers behind the charts (us per 8192 values)
Case x86 operator x86 divisor x86 speedup arm64 operator arm64 divisor arm64 speedup
u32 / 13 15.1 2.2 6.9x 31.5 1.69 18.7x
u32 % 13 15.1 4.0 3.8x 32.2 1.77 18.2x
i32 / 13 15.1 5.8 2.6x 30.0 2.1 14.3x
i32 % 13 15.1 7.7 2.0x 30.7 2.4 12.8x
u64 / 13 25.1 6.2 4.0x 53.4 5.2 10.3x
u64 % 13 24.8 9.6 2.6x 56.2 5.2 10.8x
i64 / 13 24.7 9.6 2.6x 52.0 6.0 8.7x
i64 % 13 24.7 13.6 1.8x 54.7 6.5 8.4x
u32 / 16 15.1 0.87 17.3x 13.7 0.41 33.3x
u32 % 16 15.1 0.74 20.3x 14.4 0.41 35.3x
i32 / 16 15.1 1.49 10.2x 13.7 1.32 10.4x
i32 % 16 15.1 1.13 13.4x 14.4 1.10 13.1x
u64 / 16 24.6 1.51 16.3x 13.7 0.80 17.2x
u64 % 16 24.8 1.45 17.1x 16.5 0.80 20.7x
i64 / 16 24.7 4.2 5.9x 13.7 2.6 5.3x
i64 % 16 25.0 2.6 9.6x 16.5 2.2 7.5x

@github-actions github-actions Bot added the arrow-core Core crates related to representing arrow data label Oct 12, 2026
Jefffrey pushed a commit to sdf-jkl/arrow-rs that referenced this pull request Oct 15, 2026
…ray::unary` (apache#11525)

# Which issue does this PR close?

- Part of apache#11421.

# Rationale for this change


[`PrimitiveArray::unary`](https://github.com/apache/arrow-rs/blob/01b1275aab90c033da3f64f5edbfb4f061606cae/arrow-array/src/array/primitive_array.rs#L933)
maps the values with `|v| op(*v)`, which captures `op` by reference.
When `Vec::from_iter` isn't inlined into `unary`, the loop reads the
captures of `op` through that reference and reloads them for every
value. I found this in apache#11467
([comment](apache#11467 (comment))),
where it kept LLVM from moving a `match` on the divisor out of the loop.

The decimal downscale cast on `main` has the same problem. With 16
codegen units, the default, `from_iter` for the infallible closure from
[`make_downscaler`](https://github.com/apache/arrow-rs/blob/01b1275aab90c033da3f64f5edbfb4f061606cae/arrow-cast/src/cast/decimal.rs#L238-L263)
isn't inlined. In the thin LTO disassembly for aarch64 and x86-64, the
`Decimal64` loop reloads `div`, `half` and `half_neg` for each value,
checks `div` for zero and -1, and branches on the sign of the value.
With `op` moved into the closure, it loads the captures and checks `div`
once, before the loop, and the rounding uses `csel` on aarch64 and
`cmov` on x86-64 instead of a branch.

# What changes are included in this PR?

`unary` maps the values with `move |v| op(*v)`, so the iterator holds
`op` instead of a reference to it.

# Are these changes tested?

The existing tests cover `unary`, and its results don't change.

The benchmark bot ran the `decimal_downscale` cases of the
`cast_decimal` benchmark from apache#11426 on
[x86_64](apache#11525 (comment))
(Xeon Platinum 8581C) and
[arm64](apache#11525 (comment))
(Neoverse-V2).

<img width="1600" height="1124" alt="unary-move-bot"
src="https://github.com/user-attachments/assets/c1d7d1f6-f14b-4ff6-ab1e-c7b0db5ebc9f"
/>

<details><summary>Numbers behind the chart</summary>

In us per 8192 values with no nulls:

| Case | x86_64: `main` | This PR | arm64: `main` | This PR |
|---|---|---|---|---|
| `Decimal32` (9, 4) to (9, 2) | 29.1 | 15.0 | 30.4 | 30.4 |
| `Decimal64` (18, 6) to (18, 2) | 25.3 | 27.6 | 46.7 | 46.7 |
| `Decimal128` (18, 6) to (18, 2) | 74.0 | 67.9 | 51.7 | 48.2 |
| `Decimal128` (38, 10) to (38, 2), 15 digits | 74.1 | 67.8 | 49.4 |
42.9 |
| `Decimal128` (38, 10) to (38, 2), 38 digits | 83.6 | 77.8 | 114.7 |
115.5 |
| `Decimal128` (38, 20) to (38, 2), 38 digits | 83.7 | 68.6 | 170.2 |
170.7 |

</details>

The `Decimal256` and fallible cases changed by 6% or less on both
runners. On x86_64, `Decimal64` was 9% slower without nulls and 2%
slower with nulls. `unary` computes every slot either way, so these two
cases should cost the same, and I don't know yet why they differ. The
single-value case of `decimal_downscale_small` was 8% slower on x86_64
(138 vs 150 ns).

The gain depends on the number of codegen units. These numbers are from
an Apple M5 Max with rustc 1.99.0, in us per 8192 values with no nulls.

<details><summary>Apple M5 Max by build setting</summary>

| Case | 16 codegen units, no LTO: `main` | This PR | 1 codegen unit,
fat LTO: `main` | This PR |
|---|---|---|---|---|
| `Decimal32` (9, 4) to (9, 2) | 7.25 | 4.56 | 4.50 | 4.49 |
| `Decimal64` (18, 6) to (18, 2) | 7.21 | 4.48 | 4.30 | 4.22 |
| `Decimal128` (18, 6) to (18, 2) | 21.36 | 19.52 | 20.27 | 20.10 |

</details>

Thin LTO with 16 codegen units gave a similar gain (6.06 to 4.49 us for
`Decimal32`, 6.86 to 4.27 us for `Decimal64`), and thin LTO with one
codegen unit measured the same as fat LTO. With one codegen unit, `main`
is already as fast as this PR.

# Are there any significant user-facing changes?

No.

## AI Disclosure

I wrote this PR with help from Claude but understand and support the
core approach, implementation, and test coverage.
@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

env:
  BENCH_FILTER: "round_down_|magnitude_"

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing divisor-types (7ef5ad9) to b391e5a (merge-base) diff

Run configuration
run benchmark divisor
env:
  BENCH_FILTER: round_down_|magnitude_
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

…benchmark bot

Temporary. Adds experiment types and benchmark groups so the bot can compare
figure 4.1 and 5.1 with Up, Down and devanbenz's remainder in one run. The next
commit removes them.
@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing divisor-types (7ef5ad9) to b391e5a (merge-base) diff

Run configuration
run benchmark divisor
env:
  BENCH_FILTER: round_down_|magnitude_
shards: 1

Baseline build unavailable for shard(s) 1; changed-only measurements for those workers.

Details
group                                          changed
-----                                          -------
magnitude_i32/div -13/fig5.1                   1.00      2.1±0.00µs  3.6 GElem/sec
magnitude_i32/div -13/fig5.1_magnitude         1.00      2.1±0.00µs  3.6 GElem/sec
magnitude_i32/div 13/fig5.1                    1.00      2.1±0.00µs  3.6 GElem/sec
magnitude_i32/div 13/fig5.1_magnitude          1.00      2.1±0.00µs  3.6 GElem/sec
magnitude_i32/div 7/fig5.1                     1.00      2.1±0.00µs  3.6 GElem/sec
magnitude_i32/div 7/fig5.1_magnitude           1.00      2.1±0.00µs  3.6 GElem/sec
magnitude_i32/rem -13/fig5.1                   1.00      2.4±0.00µs  3.2 GElem/sec
magnitude_i32/rem -13/fig5.1_magnitude         1.00   1784.3±2.95ns  4.3 GElem/sec
magnitude_i32/rem 13/fig5.1                    1.00      2.4±0.00µs  3.2 GElem/sec
magnitude_i32/rem 13/fig5.1_magnitude          1.00   1784.4±2.89ns  4.3 GElem/sec
magnitude_i32/rem 7/fig5.1                     1.00      2.4±0.00µs  3.2 GElem/sec
magnitude_i32/rem 7/fig5.1_magnitude           1.00   1784.3±3.00ns  4.3 GElem/sec
magnitude_i64/div -13/fig5.1                   1.00      6.0±0.00µs 1303.3 MElem/sec
magnitude_i64/div -13/fig5.1_magnitude         1.00      6.0±0.01µs 1300.5 MElem/sec
magnitude_i64/div 13/fig5.1                    1.00      6.0±0.00µs 1303.3 MElem/sec
magnitude_i64/div 13/fig5.1_magnitude          1.00      6.0±0.00µs 1303.2 MElem/sec
magnitude_i64/div 7/fig5.1                     1.00      6.0±0.00µs 1301.9 MElem/sec
magnitude_i64/div 7/fig5.1_magnitude           1.00      6.0±0.00µs 1301.7 MElem/sec
magnitude_i64/rem -13/fig5.1                   1.00      6.4±0.09µs 1215.0 MElem/sec
magnitude_i64/rem -13/fig5.1_magnitude         1.00      5.4±0.02µs 1444.2 MElem/sec
magnitude_i64/rem 13/fig5.1                    1.00      6.2±0.03µs 1260.7 MElem/sec
magnitude_i64/rem 13/fig5.1_magnitude          1.00      5.4±0.05µs 1454.4 MElem/sec
magnitude_i64/rem 7/fig5.1                     1.00      6.3±0.04µs 1247.1 MElem/sec
magnitude_i64/rem 7/fig5.1_magnitude           1.00      5.2±0.00µs 1503.2 MElem/sec
round_down_u32/div 100/fig4.1                  1.00   1695.2±3.23ns  4.5 GElem/sec
round_down_u32/div 100/fig4.2_no_pre_shift     1.00   1059.6±0.66ns  7.2 GElem/sec
round_down_u32/div 100/n_bit_dist              1.00   1202.7±2.26ns  6.3 GElem/sec
round_down_u32/div 100/n_bit_sat               1.00   1326.4±2.66ns  5.8 GElem/sec
round_down_u32/div 1000/fig4.1                 1.00   1695.9±3.65ns  4.5 GElem/sec
round_down_u32/div 1000/fig4.2_no_pre_shift    1.00   1059.7±0.70ns  7.2 GElem/sec
round_down_u32/div 1000/n_bit_dist             1.00   1203.0±2.35ns  6.3 GElem/sec
round_down_u32/div 1000/n_bit_sat              1.00   1326.5±2.70ns  5.8 GElem/sec
round_down_u32/div 13/fig4.1                   1.00   1695.5±3.17ns  4.5 GElem/sec
round_down_u32/div 13/fig4.2_no_pre_shift      1.00   1061.1±0.70ns  7.2 GElem/sec
round_down_u32/div 13/n_bit_dist               1.00   1202.6±2.31ns  6.3 GElem/sec
round_down_u32/div 13/n_bit_sat                1.00   1326.2±2.73ns  5.8 GElem/sec
round_down_u32/div 7/fig4.1                    1.00   1687.6±4.87ns  4.5 GElem/sec
round_down_u32/div 7/n_bit_dist                1.00   1203.1±2.22ns  6.3 GElem/sec
round_down_u32/div 7/n_bit_sat                 1.00   1326.6±2.67ns  5.8 GElem/sec
round_down_u32/div 7/round_down_dist           1.00      3.3±0.01µs  2.3 GElem/sec
round_down_u32/div 7/round_down_sat            1.00   1326.0±2.71ns  5.8 GElem/sec
round_down_u32/rem 100/fig4.1                  1.00   1781.2±2.97ns  4.3 GElem/sec
round_down_u32/rem 100/fig4.2_no_pre_shift     1.00   1403.8±1.96ns  5.4 GElem/sec
round_down_u32/rem 100/n_bit_dist              1.00   1491.8±2.96ns  5.1 GElem/sec
round_down_u32/rem 100/n_bit_sat               1.00   1472.4±2.96ns  5.2 GElem/sec
round_down_u32/rem 1000/fig4.1                 1.00   1780.3±3.18ns  4.3 GElem/sec
round_down_u32/rem 1000/fig4.2_no_pre_shift    1.00   1404.1±2.04ns  5.4 GElem/sec
round_down_u32/rem 1000/n_bit_dist             1.00   1495.9±2.49ns  5.1 GElem/sec
round_down_u32/rem 1000/n_bit_sat              1.00   1472.3±2.99ns  5.2 GElem/sec
round_down_u32/rem 13/fig4.1                   1.00   1781.2±3.27ns  4.3 GElem/sec
round_down_u32/rem 13/fig4.2_no_pre_shift      1.00   1403.8±2.05ns  5.4 GElem/sec
round_down_u32/rem 13/n_bit_dist               1.00   1490.6±2.44ns  5.1 GElem/sec
round_down_u32/rem 13/n_bit_sat                1.00   1472.7±3.10ns  5.2 GElem/sec
round_down_u32/rem 7/fig4.1                    1.00   1785.9±4.39ns  4.3 GElem/sec
round_down_u32/rem 7/n_bit_dist                1.00   1491.6±2.69ns  5.1 GElem/sec
round_down_u32/rem 7/n_bit_sat                 1.00   1472.9±3.05ns  5.2 GElem/sec
round_down_u32/rem 7/round_down_dist           1.00      4.3±0.00µs 1834.5 MElem/sec
round_down_u32/rem 7/round_down_sat            1.00   1477.3±3.27ns  5.2 GElem/sec
round_down_u64/div 100/fig4.1                  1.00      5.2±0.00µs 1510.4 MElem/sec
round_down_u64/div 100/n_bit_dist              1.00      5.2±0.00µs 1515.1 MElem/sec
round_down_u64/div 100/n_bit_sat               1.00      4.6±0.00µs 1692.1 MElem/sec
round_down_u64/div 100/round_down_dist         1.00      5.0±0.00µs 1572.9 MElem/sec
round_down_u64/div 100/round_down_sat          1.00      4.6±0.00µs 1693.8 MElem/sec
round_down_u64/div 1000/fig4.1                 1.00      5.2±0.00µs 1510.2 MElem/sec
round_down_u64/div 1000/n_bit_dist             1.00      5.2±0.00µs 1514.9 MElem/sec
round_down_u64/div 1000/n_bit_sat              1.00      4.6±0.01µs 1693.1 MElem/sec
round_down_u64/div 1000/round_down_dist        1.00      5.0±0.00µs 1572.9 MElem/sec
round_down_u64/div 1000/round_down_sat         1.00      4.6±0.00µs 1687.1 MElem/sec
round_down_u64/div 13/fig4.1                   1.00      5.2±0.01µs 1510.2 MElem/sec
round_down_u64/div 13/fig4.2_no_pre_shift      1.00      3.3±0.00µs  2.3 GElem/sec
round_down_u64/div 13/n_bit_dist               1.00      5.2±0.00µs 1514.5 MElem/sec
round_down_u64/div 13/n_bit_sat                1.00      4.6±0.00µs 1694.2 MElem/sec
round_down_u64/div 7/fig4.1                    1.00      5.2±0.00µs 1510.3 MElem/sec
round_down_u64/div 7/n_bit_dist                1.00      5.2±0.00µs 1513.9 MElem/sec
round_down_u64/div 7/n_bit_sat                 1.00      4.6±0.00µs 1690.7 MElem/sec
round_down_u64/div 7/round_down_dist           1.00      5.0±0.00µs 1572.5 MElem/sec
round_down_u64/div 7/round_down_sat            1.00      4.6±0.00µs 1693.9 MElem/sec
round_down_u64/rem 100/fig4.1                  1.00      5.3±0.01µs 1473.0 MElem/sec
round_down_u64/rem 100/n_bit_dist              1.00      6.1±0.00µs 1287.9 MElem/sec
round_down_u64/rem 100/n_bit_sat               1.00      4.8±0.00µs 1631.8 MElem/sec
round_down_u64/rem 100/round_down_dist         1.00      5.8±0.00µs 1348.3 MElem/sec
round_down_u64/rem 100/round_down_sat          1.00      4.8±0.00µs 1635.8 MElem/sec
round_down_u64/rem 1000/fig4.1                 1.00      5.4±0.01µs 1457.8 MElem/sec
round_down_u64/rem 1000/n_bit_dist             1.00      6.1±0.00µs 1287.7 MElem/sec
round_down_u64/rem 1000/n_bit_sat              1.00      4.8±0.00µs 1632.0 MElem/sec
round_down_u64/rem 1000/round_down_dist        1.00      5.8±0.00µs 1346.9 MElem/sec
round_down_u64/rem 1000/round_down_sat         1.00      4.8±0.00µs 1638.3 MElem/sec
round_down_u64/rem 13/fig4.1                   1.00      5.4±0.01µs 1458.3 MElem/sec
round_down_u64/rem 13/fig4.2_no_pre_shift      1.00      4.6±0.01µs 1685.9 MElem/sec
round_down_u64/rem 13/n_bit_dist               1.00      6.1±0.00µs 1287.9 MElem/sec
round_down_u64/rem 13/n_bit_sat                1.00      4.8±0.00µs 1632.3 MElem/sec
round_down_u64/rem 7/fig4.1                    1.00      5.4±0.01µs 1460.2 MElem/sec
round_down_u64/rem 7/n_bit_dist                1.00      6.1±0.01µs 1286.7 MElem/sec
round_down_u64/rem 7/n_bit_sat                 1.00      4.8±0.00µs 1632.5 MElem/sec
round_down_u64/rem 7/round_down_dist           1.00      5.8±0.00µs 1348.4 MElem/sec
round_down_u64/rem 7/round_down_sat            1.00      4.8±0.01µs 1637.9 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-1jpat00e-95e6cfb4-fdj7

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6425705572-3888-ctkc7 6.12.105+ #1 SMP Sat Sep  5 07:54:11 UTC 2026 aarch64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

branch

Metric Value
Wall time 930.2s
Peak memory 24.2 MiB
Avg memory 20.5 MiB
CPU user 924.9s
CPU sys 0.1s
Peak spill 0 B

File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing divisor-types (7ef5ad9) to b391e5a (merge-base) diff

Run configuration
run benchmark divisor
env:
  BENCH_FILTER: round_down_|magnitude_
  RUSTFLAGS: -C target-cpu=x86-64-v3
resources:
  arch: amd64
shards: 1

Baseline build unavailable for shard(s) 1; changed-only measurements for those workers.

Details
group                                          changed
-----                                          -------
magnitude_i32/div -13/fig5.1                   1.00   1195.3±7.54ns  6.4 GElem/sec
magnitude_i32/div -13/fig5.1_magnitude         1.00  1198.4±14.88ns  6.4 GElem/sec
magnitude_i32/div 13/fig5.1                    1.00  1195.5±23.99ns  6.4 GElem/sec
magnitude_i32/div 13/fig5.1_magnitude          1.00   1191.6±3.65ns  6.4 GElem/sec
magnitude_i32/div 7/fig5.1                     1.00  1195.7±22.57ns  6.4 GElem/sec
magnitude_i32/div 7/fig5.1_magnitude           1.00   1200.0±9.02ns  6.4 GElem/sec
magnitude_i32/rem -13/fig5.1                   1.00  1569.2±22.81ns  4.9 GElem/sec
magnitude_i32/rem -13/fig5.1_magnitude         1.00  1344.9±33.01ns  5.7 GElem/sec
magnitude_i32/rem 13/fig5.1                    1.00  1554.5±27.81ns  4.9 GElem/sec
magnitude_i32/rem 13/fig5.1_magnitude          1.00   1338.3±7.33ns  5.7 GElem/sec
magnitude_i32/rem 7/fig5.1                     1.00   1558.6±5.55ns  4.9 GElem/sec
magnitude_i32/rem 7/fig5.1_magnitude           1.00  1348.8±31.11ns  5.7 GElem/sec
magnitude_i64/div -13/fig5.1                   1.00      6.8±0.01µs 1143.7 MElem/sec
magnitude_i64/div -13/fig5.1_magnitude         1.00      6.9±0.09µs 1133.6 MElem/sec
magnitude_i64/div 13/fig5.1                    1.00      6.9±0.03µs 1132.9 MElem/sec
magnitude_i64/div 13/fig5.1_magnitude          1.00      6.9±0.03µs 1133.9 MElem/sec
magnitude_i64/div 7/fig5.1                     1.00      6.9±0.02µs 1134.1 MElem/sec
magnitude_i64/div 7/fig5.1_magnitude           1.00      6.9±0.02µs 1134.3 MElem/sec
magnitude_i64/rem -13/fig5.1                   1.00      8.3±0.04µs 944.6 MElem/sec
magnitude_i64/rem -13/fig5.1_magnitude         1.00      7.9±0.07µs 991.3 MElem/sec
magnitude_i64/rem 13/fig5.1                    1.00      8.4±0.04µs 926.0 MElem/sec
magnitude_i64/rem 13/fig5.1_magnitude          1.00      8.0±0.03µs 978.0 MElem/sec
magnitude_i64/rem 7/fig5.1                     1.00      8.4±0.02µs 926.1 MElem/sec
magnitude_i64/rem 7/fig5.1_magnitude           1.00      8.0±0.08µs 975.1 MElem/sec
round_down_u32/div 100/fig4.1                  1.00    993.2±5.31ns  7.7 GElem/sec
round_down_u32/div 100/fig4.2_no_pre_shift     1.00    796.7±6.25ns  9.6 GElem/sec
round_down_u32/div 100/n_bit_dist              1.00   888.2±18.85ns  8.6 GElem/sec
round_down_u32/div 100/n_bit_sat               1.00   1037.6±4.88ns  7.4 GElem/sec
round_down_u32/div 1000/fig4.1                 1.00  1006.2±29.64ns  7.6 GElem/sec
round_down_u32/div 1000/fig4.2_no_pre_shift    1.00    798.0±6.70ns  9.6 GElem/sec
round_down_u32/div 1000/n_bit_dist             1.00   890.8±11.93ns  8.6 GElem/sec
round_down_u32/div 1000/n_bit_sat              1.00  1067.9±19.18ns  7.1 GElem/sec
round_down_u32/div 13/fig4.1                   1.00    984.8±8.25ns  7.7 GElem/sec
round_down_u32/div 13/fig4.2_no_pre_shift      1.00    796.6±2.80ns  9.6 GElem/sec
round_down_u32/div 13/n_bit_dist               1.00    889.0±4.22ns  8.6 GElem/sec
round_down_u32/div 13/n_bit_sat                1.00   1040.9±8.52ns  7.3 GElem/sec
round_down_u32/div 7/fig4.1                    1.00  1018.2±16.70ns  7.5 GElem/sec
round_down_u32/div 7/n_bit_dist                1.00    904.8±5.02ns  8.4 GElem/sec
round_down_u32/div 7/n_bit_sat                 1.00  1039.0±10.52ns  7.3 GElem/sec
round_down_u32/div 7/round_down_dist           1.00  1506.8±10.27ns  5.1 GElem/sec
round_down_u32/div 7/round_down_sat            1.00  1044.7±20.05ns  7.3 GElem/sec
round_down_u32/rem 100/fig4.1                  1.00  1328.8±25.11ns  5.7 GElem/sec
round_down_u32/rem 100/fig4.2_no_pre_shift     1.00   1013.3±8.80ns  7.5 GElem/sec
round_down_u32/rem 100/n_bit_dist              1.00  1219.7±23.58ns  6.3 GElem/sec
round_down_u32/rem 100/n_bit_sat               1.00   1253.8±5.20ns  6.1 GElem/sec
round_down_u32/rem 1000/fig4.1                 1.00  1318.7±12.97ns  5.8 GElem/sec
round_down_u32/rem 1000/fig4.2_no_pre_shift    1.00   1014.9±8.26ns  7.5 GElem/sec
round_down_u32/rem 1000/n_bit_dist             1.00  1199.4±11.81ns  6.4 GElem/sec
round_down_u32/rem 1000/n_bit_sat              1.00  1291.3±49.62ns  5.9 GElem/sec
round_down_u32/rem 13/fig4.1                   1.00  1319.4±12.48ns  5.8 GElem/sec
round_down_u32/rem 13/fig4.2_no_pre_shift      1.00  1034.9±23.53ns  7.4 GElem/sec
round_down_u32/rem 13/n_bit_dist               1.00  1201.8±11.72ns  6.3 GElem/sec
round_down_u32/rem 13/n_bit_sat                1.00   1251.8±7.46ns  6.1 GElem/sec
round_down_u32/rem 7/fig4.1                    1.00  1420.2±25.03ns  5.4 GElem/sec
round_down_u32/rem 7/n_bit_dist                1.00   1200.7±5.40ns  6.4 GElem/sec
round_down_u32/rem 7/n_bit_sat                 1.00   1254.5±6.89ns  6.1 GElem/sec
round_down_u32/rem 7/round_down_dist           1.00  1830.6±11.36ns  4.2 GElem/sec
round_down_u32/rem 7/round_down_sat            1.00   1251.9±7.42ns  6.1 GElem/sec
round_down_u64/div 100/fig4.1                  1.00      4.6±0.01µs 1711.1 MElem/sec
round_down_u64/div 100/n_bit_dist              1.00      5.2±0.01µs 1503.6 MElem/sec
round_down_u64/div 100/n_bit_sat               1.00      6.3±0.02µs 1239.6 MElem/sec
round_down_u64/div 100/round_down_dist         1.00      4.9±0.08µs 1586.9 MElem/sec
round_down_u64/div 100/round_down_sat          1.00      6.3±0.03µs 1240.6 MElem/sec
round_down_u64/div 1000/fig4.1                 1.00      4.6±0.10µs 1696.5 MElem/sec
round_down_u64/div 1000/n_bit_dist             1.00      5.2±0.02µs 1502.9 MElem/sec
round_down_u64/div 1000/n_bit_sat              1.00      6.3±0.01µs 1240.0 MElem/sec
round_down_u64/div 1000/round_down_dist        1.00      4.9±0.01µs 1603.5 MElem/sec
round_down_u64/div 1000/round_down_sat         1.00      6.3±0.02µs 1242.2 MElem/sec
round_down_u64/div 13/fig4.1                   1.00      4.6±0.01µs 1710.1 MElem/sec
round_down_u64/div 13/fig4.2_no_pre_shift      1.00      4.9±0.04µs 1609.8 MElem/sec
round_down_u64/div 13/n_bit_dist               1.00      5.3±0.09µs 1485.1 MElem/sec
round_down_u64/div 13/n_bit_sat                1.00      6.3±0.01µs 1240.1 MElem/sec
round_down_u64/div 7/fig4.1                    1.00      4.6±0.02µs 1711.2 MElem/sec
round_down_u64/div 7/n_bit_dist                1.00      5.2±0.01µs 1502.3 MElem/sec
round_down_u64/div 7/n_bit_sat                 1.00      6.3±0.09µs 1236.3 MElem/sec
round_down_u64/div 7/round_down_dist           1.00      4.9±0.03µs 1600.2 MElem/sec
round_down_u64/div 7/round_down_sat            1.00      6.3±0.03µs 1240.5 MElem/sec
round_down_u64/rem 100/fig4.1                  1.00      6.0±0.01µs 1312.4 MElem/sec
round_down_u64/rem 100/n_bit_dist              1.00      5.5±0.04µs 1409.5 MElem/sec
round_down_u64/rem 100/n_bit_sat               1.00      8.4±0.04µs 930.0 MElem/sec
round_down_u64/rem 100/round_down_dist         1.00      6.2±0.17µs 1253.4 MElem/sec
round_down_u64/rem 100/round_down_sat          1.00      8.4±0.03µs 929.2 MElem/sec
round_down_u64/rem 1000/fig4.1                 1.00      6.0±0.02µs 1310.9 MElem/sec
round_down_u64/rem 1000/n_bit_dist             1.00      5.5±0.04µs 1411.7 MElem/sec
round_down_u64/rem 1000/n_bit_sat              1.00      8.5±0.10µs 919.2 MElem/sec
round_down_u64/rem 1000/round_down_dist        1.00      6.2±0.03µs 1264.2 MElem/sec
round_down_u64/rem 1000/round_down_sat         1.00      8.4±0.06µs 927.8 MElem/sec
round_down_u64/rem 13/fig4.1                   1.00      6.0±0.02µs 1311.2 MElem/sec
round_down_u64/rem 13/fig4.2_no_pre_shift      1.00      5.3±0.02µs 1487.4 MElem/sec
round_down_u64/rem 13/n_bit_dist               1.00      5.5±0.09µs 1409.5 MElem/sec
round_down_u64/rem 13/n_bit_sat                1.00      8.4±0.02µs 928.8 MElem/sec
round_down_u64/rem 7/fig4.1                    1.00      6.0±0.02µs 1311.7 MElem/sec
round_down_u64/rem 7/n_bit_dist                1.00      5.5±0.03µs 1413.0 MElem/sec
round_down_u64/rem 7/n_bit_sat                 1.00      8.4±0.02µs 928.7 MElem/sec
round_down_u64/rem 7/round_down_dist           1.00      6.2±0.08µs 1261.4 MElem/sec
round_down_u64/rem 7/round_down_sat            1.00      8.4±0.03µs 930.6 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-1j6n3dna-4c7ffb6a-dskm

Instance: c4-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6425703895-3887-hv629 6.12.105+ #1 SMP Sat Sep  5 07:57:24 UTC 2026 x86_64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            x86_64
CPU op-mode(s):                          32-bit, 64-bit
Address sizes:                           52 bits physical, 57 bits virtual
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               GenuineIntel
Model name:                              INTEL(R) XEON(R) PLATINUM 8581C CPU @ 2.30GHz
CPU family:                              6
Model:                                   207
Thread(s) per core:                      2
Core(s) per socket:                      8
Socket(s):                               1
Stepping:                                2
BogoMIPS:                                4600.00
Flags:                                   fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq monitor ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves avx_vnni avx512_bf16 wbnoinvd arat avx512vbmi umip avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid cldemote movdiri movdir64b fsrm md_clear serialize tsxldtrk amx_bf16 avx512_fp16 amx_tile amx_int8 arch_capabilities
Hypervisor vendor:                       KVM
Virtualization type:                     full
L1d cache:                               384 KiB (8 instances)
L1i cache:                               256 KiB (8 instances)
L2 cache:                                16 MiB (8 instances)
L3 cache:                                260 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; Enhanced / Automatic IBRS; IBPB conditional; PBRSB-eIBRS SW sequence; BHI BHI_DIS_S
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

branch

Metric Value
Wall time 930.2s
Peak memory 23.2 MiB
Avg memory 19.7 MiB
CPU user 923.0s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@mbutrovich

Copy link
Copy Markdown
Contributor Author

Thanks @mightsleep and @devanbenz. I adopted the u32 split, the signed remainder, and the correctness checks at the ends of the last quotient steps. The enums are now #[non_exhaustive], so a later variant isn't a breaking change.

The benchmark bot compared the new forms with the PR's previous ones in the same run, on x86_64 (Xeon Platinum 8581C, x86-64-v3) and arm64 (Neoverse-V2). The ratios are previous time over new time.

divisor-variants-bot-x86 divisor-variants-bot-arm
Case Variant x86_64 / x86_64 % arm64 / arm64 %
u32 by 13 Up 1.24x 1.27x 1.60x 1.27x
u32 by 7 Down 0.97x 1.13x 1.27x 1.21x
u64 by 13 Up 0.94x 1.13x 1.58x 1.17x
i32 by 13 AddIndicator 1.00x 1.16x 1.00x 1.34x
i64 by 13 AddIndicator 1.00x 1.05x 1.00x 1.15x

On an Apple M5 Max the same comparison gave 1.47x and 1.38x for u32 with Up, 1.19x and 1.17x for u32 with Down, 1.78x and 1.41x for u64 with Up, and 1.28x and 1.57x for the i32 and i64 remainders.

Two cases lose a little on the Xeon. u64 / with Up is 0.94x. In the thin-LTO build for x86-64-v3, that loop computes each high multiply with a scalar mulx and moves it into a vector register, and Up removes only vector instructions from it. u32 / with Down is 0.97x. Both gain 1.13x on %, and both gain on the two Arm CPUs, so I kept them.

A few things I tried and left out:

  • Down for u64 was 0.71-0.73x on the Xeon and 0.95-0.97x for % on the M5, though it gained 1.10-1.13x on Neoverse-V2.
  • Computing Down as m * n + m in the double-width product instead of a saturating increment was 0.42-0.78x on every CPU. LLVM rewrites it as m * (n + 1) in 64-bit lanes, which needs a full 64-bit multiply.
  • The direct remainder from figures 1 and 2 of Lemire, Kaser and Kurz was 0.67-0.71x on the M5. It needs a 64-bit low multiply and a 64x64 high multiply per value, so it vectorizes worse than the quotient-based remainder in 32-bit lanes.

@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
resources:
  arch: amd64

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing b78c2b4 (b78c2b4) to b78c2b4 diff

Run configuration
run benchmark divisor
baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
resources:
  arch: amd64
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing b78c2b4 (b78c2b4) to b78c2b4 diff

Run configuration
run benchmark divisor
baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

@mbutrovich

Copy link
Copy Markdown
Contributor Author

run benchmark divisor

baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
env:
  RUSTFLAGS: "-C target-cpu=x86-64-v3"
resources:
  arch: amd64

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark starting (GKE) | trigger

Target: divisor

Sharding factor: 1 (1 workers).

Comparing b78c2b4 (b78c2b4) to b78c2b4 diff

Run configuration
run benchmark divisor
baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
env:
  RUSTFLAGS: -C target-cpu=x86-64-v3
resources:
  arch: amd64
shards: 1

Results will be posted when all workers finish.


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing b78c2b4 (b78c2b4) to b78c2b4 diff

Run configuration
run benchmark divisor
baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
shards: 1
Details
group                          base                                     changed
-----                          ----                                     -------
divisor_i32/div 13/divisor     1.00      2.1±0.00µs  3.6 GElem/sec      1.00      2.1±0.00µs  3.6 GElem/sec
divisor_i32/div 13/operator    1.00     30.0±0.01µs 260.3 MElem/sec     1.00     30.0±0.08µs 260.2 MElem/sec
divisor_i32/div 16/divisor     1.01   1329.2±0.52ns  5.7 GElem/sec      1.00   1318.4±0.66ns  5.8 GElem/sec
divisor_i32/div 16/operator    1.00     13.7±0.00µs 568.4 MElem/sec     1.00     13.7±0.01µs 569.0 MElem/sec
divisor_i32/div 7/divisor      1.00      2.1±0.00µs  3.6 GElem/sec      1.00      2.1±0.00µs  3.6 GElem/sec
divisor_i32/div 7/operator     1.00     31.5±0.01µs 248.0 MElem/sec     1.00     31.5±0.01µs 248.0 MElem/sec
divisor_i32/rem 13/divisor     1.01   1779.8±0.80ns  4.3 GElem/sec      1.00   1770.2±4.24ns  4.3 GElem/sec
divisor_i32/rem 13/operator    1.00     30.7±0.02µs 254.4 MElem/sec     1.00     30.7±0.07µs 254.3 MElem/sec
divisor_i32/rem 16/divisor     1.01   1112.5±0.33ns  6.9 GElem/sec      1.00   1101.5±0.56ns  6.9 GElem/sec
divisor_i32/rem 16/operator    1.00     14.4±0.00µs 541.5 MElem/sec     1.00     14.4±0.01µs 541.8 MElem/sec
divisor_i32/rem 7/divisor      1.01   1780.1±0.95ns  4.3 GElem/sec      1.00   1769.2±1.00ns  4.3 GElem/sec
divisor_i32/rem 7/operator     1.00     32.2±0.01µs 242.6 MElem/sec     1.00     32.2±0.03µs 242.7 MElem/sec
divisor_i64/div 13/divisor     1.00      6.0±0.00µs 1306.6 MElem/sec    1.00      6.0±0.00µs 1306.6 MElem/sec
divisor_i64/div 13/operator    1.00     52.0±0.09µs 150.3 MElem/sec     1.00     52.0±0.01µs 150.3 MElem/sec
divisor_i64/div 16/divisor     1.00      2.6±0.00µs  2.9 GElem/sec      1.00      2.6±0.00µs  2.9 GElem/sec
divisor_i64/div 16/operator    1.00     13.7±0.00µs 568.9 MElem/sec     1.00     13.7±0.00µs 568.9 MElem/sec
divisor_i64/div 7/divisor      1.00      6.0±0.00µs 1306.7 MElem/sec    1.00      6.0±0.00µs 1306.3 MElem/sec
divisor_i64/div 7/operator     1.00     53.5±0.01µs 146.1 MElem/sec     1.00     53.5±0.08µs 146.1 MElem/sec
divisor_i64/rem 13/divisor     1.00      5.1±0.02µs 1520.0 MElem/sec    1.00      5.1±0.02µs 1521.4 MElem/sec
divisor_i64/rem 13/operator    1.00     54.7±0.03µs 142.8 MElem/sec     1.00     54.7±0.02µs 142.8 MElem/sec
divisor_i64/rem 16/divisor     1.00      2.2±0.00µs  3.5 GElem/sec      1.00      2.2±0.00µs  3.5 GElem/sec
divisor_i64/rem 16/operator    1.00     16.5±0.01µs 474.0 MElem/sec     1.00     16.5±0.03µs 473.4 MElem/sec
divisor_i64/rem 7/divisor      1.00      5.1±0.02µs 1519.0 MElem/sec    1.00      5.1±0.02µs 1522.1 MElem/sec
divisor_i64/rem 7/operator     1.00     56.2±0.02µs 139.0 MElem/sec     1.00     56.2±0.01µs 139.0 MElem/sec
divisor_u32/div 13/divisor     1.00   1049.6±0.51ns  7.3 GElem/sec      1.00   1049.7±0.52ns  7.3 GElem/sec
divisor_u32/div 13/operator    1.00     31.5±0.01µs 248.1 MElem/sec     1.00     31.5±0.02µs 248.1 MElem/sec
divisor_u32/div 16/divisor     1.00    407.2±0.31ns 18.7 GElem/sec      1.00    407.2±0.21ns 18.7 GElem/sec
divisor_u32/div 16/operator    1.00     13.7±0.01µs 569.1 MElem/sec     1.00     13.7±0.01µs 569.1 MElem/sec
divisor_u32/div 7/divisor      1.00   1336.5±0.59ns  5.7 GElem/sec      1.00   1336.8±0.66ns  5.7 GElem/sec
divisor_u32/div 7/operator     1.00     32.2±0.05µs 242.4 MElem/sec     1.00     32.2±0.01µs 242.5 MElem/sec
divisor_u32/rem 13/divisor     1.00   1392.6±0.82ns  5.5 GElem/sec      1.00   1392.5±0.62ns  5.5 GElem/sec
divisor_u32/rem 13/operator    1.00     32.2±0.02µs 242.7 MElem/sec     1.00     32.2±0.01µs 242.8 MElem/sec
divisor_u32/rem 16/divisor     1.00    408.4±1.52ns 18.7 GElem/sec      1.00    408.4±1.56ns 18.7 GElem/sec
divisor_u32/rem 16/operator    1.00     14.4±0.01µs 541.9 MElem/sec     1.00     14.4±0.01µs 541.8 MElem/sec
divisor_u32/rem 7/divisor      1.00   1460.1±2.18ns  5.2 GElem/sec      1.00   1460.1±0.74ns  5.2 GElem/sec
divisor_u32/rem 7/operator     1.00     32.9±0.02µs 237.4 MElem/sec     1.00     32.9±0.01µs 237.4 MElem/sec
divisor_u64/div 13/divisor     1.00      3.6±0.00µs  2.1 GElem/sec      1.00      3.6±0.01µs  2.1 GElem/sec
divisor_u64/div 13/operator    1.00     53.7±0.02µs 145.5 MElem/sec     1.00     53.4±0.04µs 146.2 MElem/sec
divisor_u64/div 16/divisor     1.01    805.8±0.82ns  9.5 GElem/sec      1.00    795.5±1.20ns  9.6 GElem/sec
divisor_u64/div 16/operator    1.00     13.7±0.01µs 568.5 MElem/sec     1.00     13.7±0.01µs 568.8 MElem/sec
divisor_u64/div 7/divisor      1.00      5.2±0.00µs 1513.4 MElem/sec    1.00      5.2±0.00µs 1514.9 MElem/sec
divisor_u64/div 7/operator     1.00     54.2±0.01µs 144.2 MElem/sec     1.00     54.2±0.02µs 144.2 MElem/sec
divisor_u64/rem 13/divisor     1.00      3.9±0.01µs 2016.2 MElem/sec    1.03      4.0±0.14µs 1961.2 MElem/sec
divisor_u64/rem 13/operator    1.00     56.2±0.02µs 139.1 MElem/sec     1.00     56.2±0.04µs 139.1 MElem/sec
divisor_u64/rem 16/divisor     1.01    805.7±0.80ns  9.5 GElem/sec      1.00    795.9±1.11ns  9.6 GElem/sec
divisor_u64/rem 16/operator    1.00     16.5±0.01µs 473.8 MElem/sec     1.00     16.5±0.02µs 473.9 MElem/sec
divisor_u64/rem 7/divisor      1.02      6.0±0.02µs 1295.9 MElem/sec    1.00      5.9±0.05µs 1316.4 MElem/sec
divisor_u64/rem 7/operator     1.00     56.9±0.02µs 137.2 MElem/sec     1.00     56.9±0.04µs 137.2 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-i674pijq-344a8a76-hlnd

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6426392601-3892-mgcp6 6.12.105+ #1 SMP Sat Sep  5 07:54:11 UTC 2026 aarch64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

base (merge-base)

Metric Value
Wall time 450.1s
Peak memory 10.1 MiB
Avg memory 9.8 MiB
CPU user 448.4s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 450.1s
Peak memory 10.1 MiB
Avg memory 9.8 MiB
CPU user 447.5s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing b78c2b4 (b78c2b4) to b78c2b4 diff

Run configuration
run benchmark divisor
baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
resources:
  arch: amd64
shards: 1
Details
group                          base                                     changed
-----                          ----                                     -------
divisor_i32/div 13/divisor     1.18      5.9±0.14µs 1329.7 MElem/sec    1.00      5.0±0.13µs 1570.3 MElem/sec
divisor_i32/div 13/operator    1.22     15.4±0.32µs 507.3 MElem/sec     1.00     12.6±0.42µs 619.6 MElem/sec
divisor_i32/div 16/divisor     1.10  1475.8±79.60ns  5.2 GElem/sec      1.00  1339.5±67.83ns  5.7 GElem/sec
divisor_i32/div 16/operator    1.21     15.2±0.28µs 514.6 MElem/sec     1.00     12.5±0.33µs 624.9 MElem/sec
divisor_i32/div 7/divisor      1.05      5.3±0.55µs 1483.6 MElem/sec    1.00      5.0±0.29µs 1562.7 MElem/sec
divisor_i32/div 7/operator     1.00     12.7±0.52µs 616.7 MElem/sec     1.01     12.8±0.61µs 611.3 MElem/sec
divisor_i32/rem 13/divisor     1.19      7.3±0.19µs 1063.7 MElem/sec    1.00      6.2±0.23µs 1266.9 MElem/sec
divisor_i32/rem 13/operator    1.22     15.3±0.39µs 511.4 MElem/sec     1.00     12.5±0.35µs 623.4 MElem/sec
divisor_i32/rem 16/divisor     1.17  1112.1±38.76ns  6.9 GElem/sec      1.00   947.9±57.47ns  8.0 GElem/sec
divisor_i32/rem 16/operator    1.19     15.2±0.45µs 513.0 MElem/sec     1.00     12.8±0.66µs 612.1 MElem/sec
divisor_i32/rem 7/divisor      1.28      7.6±0.03µs 1029.8 MElem/sec    1.00      5.9±0.15µs 1317.2 MElem/sec
divisor_i32/rem 7/operator     1.28     16.0±0.05µs 488.6 MElem/sec     1.00     12.5±0.33µs 623.6 MElem/sec
divisor_i64/div 13/divisor     1.01      8.1±0.19µs 962.6 MElem/sec     1.00      8.0±0.07µs 970.8 MElem/sec
divisor_i64/div 13/operator    1.01     20.9±0.49µs 373.0 MElem/sec     1.00     20.7±0.50µs 376.8 MElem/sec
divisor_i64/div 16/divisor     1.29      4.6±0.04µs 1704.9 MElem/sec    1.00      3.6±0.05µs  2.1 GElem/sec
divisor_i64/div 16/operator    1.29     26.6±0.02µs 293.3 MElem/sec     1.00     20.6±0.18µs 378.6 MElem/sec
divisor_i64/div 7/divisor      1.21      9.8±0.28µs 797.6 MElem/sec     1.00      8.1±0.23µs 967.3 MElem/sec
divisor_i64/div 7/operator     1.26     25.9±0.51µs 301.2 MElem/sec     1.00     20.6±0.15µs 378.4 MElem/sec
divisor_i64/rem 13/divisor     1.28      7.7±0.09µs 1016.2 MElem/sec    1.00      6.0±0.16µs 1302.3 MElem/sec
divisor_i64/rem 13/operator    1.05     21.6±1.02µs 362.1 MElem/sec     1.00     20.6±0.21µs 378.4 MElem/sec
divisor_i64/rem 16/divisor     1.28      2.8±0.02µs  2.8 GElem/sec      1.00      2.1±0.07µs  3.6 GElem/sec
divisor_i64/rem 16/operator    1.27     26.4±0.37µs 295.8 MElem/sec     1.00     20.7±0.45µs 377.0 MElem/sec
divisor_i64/rem 7/divisor      1.02      6.1±0.21µs 1286.3 MElem/sec    1.00      6.0±0.16µs 1308.3 MElem/sec
divisor_i64/rem 7/operator     1.02     21.0±0.56µs 372.0 MElem/sec     1.00     20.6±0.19µs 379.0 MElem/sec
divisor_u32/div 13/divisor     1.00  1285.7±25.65ns  5.9 GElem/sec      1.28  1639.6±12.60ns  4.7 GElem/sec
divisor_u32/div 13/operator    1.00     12.5±0.21µs 625.1 MElem/sec     1.28     16.0±0.10µs 489.6 MElem/sec
divisor_u32/div 16/divisor     1.00   641.6±20.80ns 11.9 GElem/sec      1.09   698.0±51.40ns 10.9 GElem/sec
divisor_u32/div 16/operator    1.00     12.5±0.19µs 625.3 MElem/sec     1.07     13.3±0.64µs 586.6 MElem/sec
divisor_u32/div 7/divisor      1.00  1847.6±50.79ns  4.1 GElem/sec      1.27      2.3±0.01µs  3.3 GElem/sec
divisor_u32/div 7/operator     1.00     12.6±0.39µs 620.1 MElem/sec     1.27     16.0±0.03µs 488.7 MElem/sec
divisor_u32/rem 13/divisor     1.00      2.6±0.06µs  2.9 GElem/sec      1.13      2.9±0.19µs  2.6 GElem/sec
divisor_u32/rem 13/operator    1.00     12.5±0.23µs 626.1 MElem/sec     1.28     16.0±0.02µs 489.8 MElem/sec
divisor_u32/rem 16/divisor     1.10   706.2±17.37ns 10.8 GElem/sec      1.00   640.4±19.60ns 11.9 GElem/sec
divisor_u32/rem 16/operator    1.00     12.6±0.45µs 622.1 MElem/sec     1.03     12.9±0.74µs 604.8 MElem/sec
divisor_u32/rem 7/divisor      1.00      3.5±0.23µs  2.2 GElem/sec      1.21      4.2±0.01µs 1846.4 MElem/sec
divisor_u32/rem 7/operator     1.00     12.8±0.66µs 611.9 MElem/sec     1.25     16.0±0.01µs 489.5 MElem/sec
divisor_u64/div 13/divisor     1.23      5.2±0.12µs 1512.0 MElem/sec    1.00      4.2±0.13µs 1867.4 MElem/sec
divisor_u64/div 13/operator    1.22     25.1±0.76µs 311.1 MElem/sec     1.00     20.7±0.34µs 378.3 MElem/sec
divisor_u64/div 16/divisor     1.27  1587.1±40.17ns  4.8 GElem/sec      1.00  1250.0±17.30ns  6.1 GElem/sec
divisor_u64/div 16/operator    1.27     26.2±0.46µs 298.3 MElem/sec     1.00     20.6±0.24µs 378.5 MElem/sec
divisor_u64/div 7/divisor      1.14      5.9±0.13µs 1316.1 MElem/sec    1.00      5.2±0.27µs 1504.7 MElem/sec
divisor_u64/div 7/operator     1.25     26.1±0.49µs 299.5 MElem/sec     1.00     20.8±0.42µs 375.4 MElem/sec
divisor_u64/rem 13/divisor     1.25      5.3±0.13µs 1485.7 MElem/sec    1.00      4.2±0.12µs 1861.5 MElem/sec
divisor_u64/rem 13/operator    1.26     26.0±0.51µs 300.5 MElem/sec     1.00     20.6±0.22µs 378.3 MElem/sec
divisor_u64/rem 16/divisor     1.25  1498.1±28.79ns  5.1 GElem/sec      1.00  1202.3±11.00ns  6.3 GElem/sec
divisor_u64/rem 16/operator    1.28     26.6±0.18µs 294.1 MElem/sec     1.00     20.7±0.51µs 377.3 MElem/sec
divisor_u64/rem 7/divisor      1.22      9.8±0.29µs 795.2 MElem/sec     1.00      8.1±0.20µs 968.5 MElem/sec
divisor_u64/rem 7/operator     1.25     25.9±0.55µs 302.0 MElem/sec     1.00     20.6±0.17µs 378.5 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-1rw85303-e515ed6b-fnbk

Instance: c4-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6426389916-3891-blxbq 6.12.105+ #1 SMP Sat Sep  5 07:57:24 UTC 2026 x86_64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            x86_64
CPU op-mode(s):                          32-bit, 64-bit
Address sizes:                           52 bits physical, 57 bits virtual
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               GenuineIntel
Model name:                              INTEL(R) XEON(R) PLATINUM 8581C CPU @ 2.30GHz
CPU family:                              6
Model:                                   207
Thread(s) per core:                      2
Core(s) per socket:                      8
Socket(s):                               1
Stepping:                                2
BogoMIPS:                                4600.00
Flags:                                   fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq monitor ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves avx_vnni avx512_bf16 wbnoinvd arat avx512vbmi umip avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid cldemote movdiri movdir64b fsrm md_clear serialize tsxldtrk amx_bf16 avx512_fp16 amx_tile amx_int8 arch_capabilities
Hypervisor vendor:                       KVM
Virtualization type:                     full
L1d cache:                               384 KiB (8 instances)
L1i cache:                               256 KiB (8 instances)
L2 cache:                                16 MiB (8 instances)
L3 cache:                                260 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; Enhanced / Automatic IBRS; IBPB conditional; PBRSB-eIBRS SW sequence; BHI BHI_DIS_S
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

base (merge-base)

Metric Value
Wall time 455.1s
Peak memory 11.7 MiB
Avg memory 11.1 MiB
CPU user 450.3s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 470.1s
Peak memory 11.0 MiB
Avg memory 10.6 MiB
CPU user 463.5s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Target: divisor

Comparing b78c2b4 (b78c2b4) to b78c2b4 diff

Run configuration
run benchmark divisor
baseline:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
changed:
  ref: b78c2b43ab3feb8aed7c7c0dec922f5d45bf7ae4
env:
  RUSTFLAGS: -C target-cpu=x86-64-v3
resources:
  arch: amd64
shards: 1
Details
group                          base                                     changed
-----                          ----                                     -------
divisor_i32/div 13/divisor     1.00  1156.5±20.76ns  6.6 GElem/sec      1.10  1274.6±13.81ns  6.0 GElem/sec
divisor_i32/div 13/operator    1.00     12.4±0.03µs 631.4 MElem/sec     1.00     12.4±0.30µs 629.7 MElem/sec
divisor_i32/div 16/divisor     1.01   673.7±12.53ns 11.3 GElem/sec      1.00    667.5±7.83ns 11.4 GElem/sec
divisor_i32/div 16/operator    1.00     12.4±0.10µs 631.1 MElem/sec     1.00     12.4±0.13µs 631.8 MElem/sec
divisor_i32/div 7/divisor      1.00  1158.1±17.16ns  6.6 GElem/sec      1.12  1300.6±34.25ns  5.9 GElem/sec
divisor_i32/div 7/operator     1.00     12.4±0.29µs 628.6 MElem/sec     1.00     12.4±0.27µs 628.7 MElem/sec
divisor_i32/rem 13/divisor     1.00  1258.9±15.08ns  6.1 GElem/sec      1.08  1358.0±40.26ns  5.6 GElem/sec
divisor_i32/rem 13/operator    1.00     12.4±0.13µs 630.5 MElem/sec     1.00     12.4±0.12µs 632.0 MElem/sec
divisor_i32/rem 16/divisor     1.02   659.6±12.82ns 11.6 GElem/sec      1.00   643.5±20.49ns 11.9 GElem/sec
divisor_i32/rem 16/operator    1.01     12.4±0.31µs 628.1 MElem/sec     1.00     12.4±0.08µs 632.2 MElem/sec
divisor_i32/rem 7/divisor      1.01  1272.0±31.58ns  6.0 GElem/sec      1.00  1255.6±13.30ns  6.1 GElem/sec
divisor_i32/rem 7/operator     1.00     12.4±0.06µs 630.8 MElem/sec     1.00     12.4±0.20µs 631.1 MElem/sec
divisor_i64/div 13/divisor     1.00      7.2±0.05µs 1087.1 MElem/sec    1.02      7.3±0.10µs 1066.0 MElem/sec
divisor_i64/div 13/operator    1.00     20.6±0.29µs 378.9 MElem/sec     1.01     20.8±0.82µs 375.5 MElem/sec
divisor_i64/div 16/divisor     1.07  1751.4±48.45ns  4.4 GElem/sec      1.00  1636.8±14.70ns  4.7 GElem/sec
divisor_i64/div 16/operator    1.00     20.6±0.28µs 378.7 MElem/sec     1.00     20.6±0.28µs 378.6 MElem/sec
divisor_i64/div 7/divisor      1.00      7.2±0.08µs 1084.3 MElem/sec    1.02      7.3±0.19µs 1063.0 MElem/sec
divisor_i64/div 7/operator     1.00     20.7±0.53µs 377.2 MElem/sec     1.00     20.6±0.30µs 378.6 MElem/sec
divisor_i64/rem 13/divisor     1.00      8.5±0.12µs 918.2 MElem/sec     1.04      8.8±0.40µs 885.4 MElem/sec
divisor_i64/rem 13/operator    1.00     20.7±0.57µs 377.2 MElem/sec     1.00     20.7±0.52µs 377.5 MElem/sec
divisor_i64/rem 16/divisor     1.22  1625.0±53.91ns  4.7 GElem/sec      1.00  1333.7±12.51ns  5.7 GElem/sec
divisor_i64/rem 16/operator    1.00     20.6±0.25µs 378.9 MElem/sec     1.01     20.8±0.67µs 375.9 MElem/sec
divisor_i64/rem 7/divisor      1.00      8.5±0.21µs 915.1 MElem/sec     1.02      8.7±0.09µs 896.9 MElem/sec
divisor_i64/rem 7/operator     1.00     20.7±0.32µs 378.0 MElem/sec     1.00     20.7±0.61µs 377.1 MElem/sec
divisor_u32/div 13/divisor     1.00   820.6±22.77ns  9.3 GElem/sec      1.06   869.7±66.06ns  8.8 GElem/sec
divisor_u32/div 13/operator    1.00     12.4±0.09µs 632.6 MElem/sec     1.01     12.5±0.44µs 625.3 MElem/sec
divisor_u32/div 16/divisor     1.00   648.8±21.01ns 11.8 GElem/sec      1.08   703.4±13.71ns 10.8 GElem/sec
divisor_u32/div 16/operator    1.00     12.4±0.23µs 630.8 MElem/sec     1.00     12.4±0.17µs 631.1 MElem/sec
divisor_u32/div 7/divisor      1.00  1052.6±11.45ns  7.2 GElem/sec      1.00   1049.1±7.02ns  7.3 GElem/sec
divisor_u32/div 7/operator     1.00     12.3±0.01µs 632.8 MElem/sec     1.00     12.4±0.27µs 630.2 MElem/sec
divisor_u32/rem 13/divisor     1.03  1144.5±27.64ns  6.7 GElem/sec      1.00  1113.9±66.12ns  6.8 GElem/sec
divisor_u32/rem 13/operator    1.02     12.6±0.60µs 622.1 MElem/sec     1.00     12.3±0.05µs 632.7 MElem/sec
divisor_u32/rem 16/divisor     1.00    696.0±8.70ns 11.0 GElem/sec      1.02   709.2±26.59ns 10.8 GElem/sec
divisor_u32/rem 16/operator    1.00     12.4±0.08µs 632.1 MElem/sec     1.01     12.5±0.39µs 627.3 MElem/sec
divisor_u32/rem 7/divisor      1.00   1374.9±5.50ns  5.5 GElem/sec      1.01  1382.9±44.62ns  5.5 GElem/sec
divisor_u32/rem 7/operator     1.00     12.4±0.39µs 628.6 MElem/sec     1.00     12.4±0.15µs 631.1 MElem/sec
divisor_u64/div 13/divisor     1.05      6.3±0.11µs 1244.0 MElem/sec    1.00      6.0±0.12µs 1301.0 MElem/sec
divisor_u64/div 13/operator    1.00     20.7±0.33µs 377.8 MElem/sec     1.00     20.7±0.45µs 378.0 MElem/sec
divisor_u64/div 16/divisor     1.05  1293.5±14.35ns  5.9 GElem/sec      1.00  1231.3±21.02ns  6.2 GElem/sec
divisor_u64/div 16/operator    1.00     20.7±0.49µs 377.4 MElem/sec     1.00     20.6±0.26µs 378.6 MElem/sec
divisor_u64/div 7/divisor      1.05      5.2±0.03µs 1511.0 MElem/sec    1.00      4.9±0.12µs 1588.5 MElem/sec
divisor_u64/div 7/operator     1.00     20.7±0.42µs 377.7 MElem/sec     1.00     20.7±0.35µs 378.1 MElem/sec
divisor_u64/rem 13/divisor     1.00      5.6±0.12µs 1394.1 MElem/sec    1.00      5.6±0.12µs 1390.6 MElem/sec
divisor_u64/rem 13/operator    1.00     20.7±0.46µs 377.9 MElem/sec     1.00     20.6±0.16µs 379.2 MElem/sec
divisor_u64/rem 16/divisor     1.04   1286.9±8.05ns  5.9 GElem/sec      1.00  1234.2±32.45ns  6.2 GElem/sec
divisor_u64/rem 16/operator    1.00     20.6±0.21µs 378.8 MElem/sec     1.00     20.7±0.50µs 378.0 MElem/sec
divisor_u64/rem 7/divisor      1.00      6.4±0.05µs 1219.7 MElem/sec    1.00      6.4±0.05µs 1218.8 MElem/sec
divisor_u64/rem 7/operator     1.01     20.8±1.10µs 374.7 MElem/sec     1.00     20.6±0.20µs 379.1 MElem/sec

Per-runner information
divisor — shard 1/1

Node: gk3-benchmark-cluster-nap-1rw85303-5be73ff4-5bnj

Instance: c4-highmem-16 (12 vCPU / 65 GiB)

uname:

Linux bench-c6426414056-3893-c9s7g 6.12.105+ #1 SMP Sat Sep  5 07:57:24 UTC 2026 x86_64 GNU/Linux

BENCH_COMMAND:

cargo bench --features=arrow,async,test_common,experimental,object_store --bench divisor
CPU Details (lscpu)
Architecture:                            x86_64
CPU op-mode(s):                          32-bit, 64-bit
Address sizes:                           52 bits physical, 57 bits virtual
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               GenuineIntel
Model name:                              INTEL(R) XEON(R) PLATINUM 8581C CPU @ 2.30GHz
CPU family:                              6
Model:                                   207
Thread(s) per core:                      2
Core(s) per socket:                      8
Socket(s):                               1
Stepping:                                2
BogoMIPS:                                4600.00
Flags:                                   fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq monitor ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves avx_vnni avx512_bf16 wbnoinvd arat avx512vbmi umip avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid cldemote movdiri movdir64b fsrm md_clear serialize tsxldtrk amx_bf16 avx512_fp16 amx_tile amx_int8 arch_capabilities
Hypervisor vendor:                       KVM
Virtualization type:                     full
L1d cache:                               384 KiB (8 instances)
L1i cache:                               256 KiB (8 instances)
L2 cache:                                16 MiB (8 instances)
L3 cache:                                260 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; Enhanced / Automatic IBRS; IBPB conditional; PBRSB-eIBRS SW sequence; BHI BHI_DIS_S
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Resource Usage

base (merge-base)

Metric Value
Wall time 470.1s
Peak memory 11.7 MiB
Avg memory 11.1 MiB
CPU user 467.4s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 475.1s
Peak memory 11.1 MiB
Avg memory 10.8 MiB
CPU user 471.5s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@mightsleep mightsleep 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.

LGTM! Ran the divisor tests on x86-64 (AddIndicator path) and x86-64-v2 (Down path), and checked the new figure 4.1 multiplier against the direct floor(2^(N+l) / d) - 2^N + 1 for every u32 divisor, everything ok.

On the signed side I have a few more tricks up my sleeve, not sure what you'd want to do with them though, so just dropping them here, maybe they could go into follow-ups, if you're interested. They should be faster than the classic forms, mostly on x86:

|n| has one bit less than the word, so the rounded-up multiplier is exact for every divisor and the quotient is an unsigned mulhi, no signed one needed:

let s = n >> 31;
let a = (n ^ s).wrapping_sub(s) as u32;
let q = (mulhi(a, m) >> p) as i32;
let qs = s ^ d_sign;
(q ^ qs).wrapping_sub(qs)

For i32, every value fits in f64 exactly, so the quotient can be a float multiply. With 1/|d| rounded up by one ulp, n * rcp never lands below the true quotient, and its error stays smaller than the gap to the next integer, so truncating gives exactly n / d for every pair. as saturates and checks, so it needs to_int_unchecked, which is a plain cvttpd2dq; the only out-of-range case is MIN / -1, and ±1 are in the power-of-two variant:

let up = f64::from_bits((1.0 / d.unsigned_abs() as f64).to_bits() + 1);
let rcp = if d < 0 { -up } else { up };
// SAFETY: |n * rcp| < 2^31 for |d| >= 2
let q = unsafe { (n as f64 * rcp).to_int_unchecked::<i32>() };

div_euclid by a positive d through the one's complement, which has 31 bits, so it's one unsigned mulhi and three ops around it:

let s = n >> 31;
((mulhi((n ^ s) as u32, m) >> p) as i32) ^ s

@mbutrovich

Copy link
Copy Markdown
Contributor Author

To check that the API works in a kernel, I prototyped integer / and % by a scalar in arrow_arith::numeric on top of this PR, with two ways to match on the variants once.

  • A macro generates one function per type, which matches on that type's variants and calls unary. arithmetic_op calls it for Int32, Int64, UInt32 and UInt64 before integer_op, which can't match on per-type variants because it is generic.
  • A visitor: the divisor matches on its own variants and passes the variant to a generic visitor, which needs four new sealed traits in arrow-buffer.

On an Apple M5 Max with the integer_division benchmark from #11427, the two were within 4% of each other in all 28 cases, in the default and thin LTO builds, and 1.2-13.5x faster than main.

Since they perform the same, I'd use the macro and keep this PR's API as it is, without the traits. The cost is that each kernel lists each type's variants, and because the enums are #[non_exhaustive], a variant added later reaches the kernel's wildcard arm, which matches on each value until the kernel lists it.

@devanbenz devanbenz 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.

Nice, the changes made from my request look good. Thanks!

@andygrove andygrove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks for addressing my earlier comments!

@sdf-jkl

sdf-jkl commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

@mbutrovich I left my agent off the leash and it produced some (more or less) improvements on top of your perf results. I'll file a gist to show the diff, so you can try it out.

Or file a draft PR and ask the bot to run the bench between it and this PR 🤷

The bench results will be here -- #11979

🤖 💬 : All nine bot comparisons have completed, including the additional SVE, SVE2, and Neoverse-V2 runs. The rewrite has substantial x86 wins, but the ARM results remain mixed.

Target i32 division i32 remainder i64 division i64 remainder
x86-64 2.08–2.09× 1.73–1.76× 1.13× 0.96×
x86-64-v2 1.85–1.97× 1.81–1.87× 1.16–1.18× 1.70–1.72×
x86-64-v3 0.76–0.81× 0.90–0.97× 1.06–1.07× 1.03–1.07×
x86-64-v4 0.91–0.92× 0.92–0.93× 1.27–1.28× 1.34–1.35×
x86 native 0.91–0.95× 0.93–0.94× 1.20–1.22× 1.32–1.33×
ARM default (NEON) 0.88× 0.85× 0.99× 0.76×
ARM +sve 0.93× 0.92× 0.99× 1.02×
ARM +sve2 0.93× 0.91× 0.99× 0.96–1.01×
ARM neoverse-v2 0.96× 0.96× 0.99× 1.06×

@mbutrovich

mbutrovich commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for running all nine comparisons, @sdf-jkl.

The rewrite has substantial x86 wins, but the ARM results remain mixed.

I'd like to keep figure 5.1 as it is in this PR. The biggest wins are on x86-64 before the v3 target (i.e., without AVX2, which are pretty rare anymore): 1.7-2.1x for i32 at the baseline target and at x86-64-v2, and 1.7x for i64 % at x86-64-v2. The other large win is i64 on AVX-512, at 1.2-1.35x. On the targets that DataFusion and Comet typically target, the rewrite is slower for i32. It runs at 0.76-0.97x at x86-64-v3 and 0.85-0.88x at the default arm64 target, and i64 % runs at 0.76x on arm64. DataFusion recommends target-cpu=native, and Comet publishes binaries built for x86-64-v3 and neoverse-n1 (apache/datafusion-comet#3368).

The generated code explains the split. On baseline x86-64 and at x86-64-v2, LLVM doesn't use a signed widening multiply for MULSH. It sign-extends each lane and builds a 64-bit product from three pmuludq per two lanes, so the unsigned form cuts the i32 loop from 36 to 20 instructions per four values. At x86-64-v3 LLVM uses vpmuldq, so there is nothing to save. On aarch64, smull and smulh compute the signed high product directly, so the sign correction is extra work. The i32 loop grows from 16 to 20 instructions per eight values, and the i64 % loop loses its 4x unrolling.

Taking only the wins would need a variant per target and per type. The kernel PR will measure each type through unary, and we can revisit the unsigned form there if baseline x86-64 matters. If we do, I'd cite program (1) in section 8-3 of Warren, "Hacker's Delight", 2nd edition. Your rewrite is that program with the last term equal to n, because m' is always negative.

This branch has not been deployed

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

Labels

arrow Changes to the arrow crate arrow-buffer arrow-core Core crates related to representing arrow data performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants