Skip to content

FBig add/sub rounding fixes, BMI2 addmul kernels, rand 0.9/0.10 support - #81

Merged
cmpute merged 17 commits into
masterfrom
misc-optim
Jun 21, 2026
Merged

FBig add/sub rounding fixes, BMI2 addmul kernels, rand 0.9/0.10 support#81
cmpute merged 17 commits into
masterfrom
misc-optim

Conversation

@cmpute

@cmpute cmpute commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Summary

A batch of correctness fixes and performance work across dashu-int and dashu-float, plus optional support for rand 0.9 / 0.10. Several of the float fixes were surfaced and are now permanently
guarded by a differential fuzz test.

dashu-float add/sub correctness

Four rounding defects in the limited-precision alignment/rounding path:

  • Severe cancellation — when the smaller operand reached into the larger operand's significant digits, the alignment path could collapse a genuine difference (e.g. 1.00 − 0.99999999 at p=3
    returned 0 instead of 1e-8). Such cases now form the exact difference at full width and round once.
  • Spurious ULP on a negligible operand — the sticky was positioned at precision − digits, letting it land on a rounding tie (e.g. 1 + 2⁻¹⁰⁰ at p=10 returned 513·2⁻⁹ instead of 1). It is
    now placed at the operand's real magnitude.
  • Window-edge boundary — the cancellation guard fired only on strict overlap, missing the edge case (0.5 − 0.4375 at p=1 returned 0). It now fires on >=.
  • Context::sub(0, b) under directed modes — the zero-left path did -round(b) instead of round(-b), mis-rounding under the asymmetric modes Up/Down. It now rounds the negated operand
    directly.

By design, an inexact addition/subtraction may now carry a single guard digit (up to precision + 1 digits); this is documented in AGENTS.md.

dashu-int performance

  • Basecase addmul/submul-2 kernels (add_mul_dword_same_len_in_place, sub_mul_dword_same_len_in_place) dispatch at runtime to a BMI2 build on x86-64 + std (flag-free mulx widening
    multiply, unrolled loop), ~4–5% faster in isolation. The portable path, no_std, and other targets are unchanged.
  • Non-power-of-2 radix formatting preallocates radix_powers/big_chunks instead of growing them push-by-push.
  • New Performance guide page (guide/src/performance.md) covering target-cpu=native and related build flags.

rand 0.9 / 0.10 support

New optional rand_v09 (rand 0.9) and rand_v010 (rand 0.10) features across dashu-int/-float/-ratio (plus root forwarding), mirroring rand_v08. The v08 source files are renamed to
rand_v08.rs for parallel naming, with a rand re-export alias preserving the existing dashu::*::rand paths. The default rand feature stays rand_v08.

rand 0.10 requires Rust 1.85; it's optional and is not enabled by the MSRV (1.68) CI path (which uses the explicit randrand_v08 feature set), so the MSRV is unchanged. rand 0.9 (MSRV
1.63) is MSRV-compatible.

Tests

  • Consolidated the standalone add_sub_oracle example into the fuzz differential test: significands drawn directly via UniformBits, a deterministic small-precision sweep, and a
    guard-digit-tolerant oracle (accept rounding to p or p+1).
  • Added a regression test for the Context::sub(0, b) directed-rounding fix.

Cleanup

Removed an unused BMI2 addmul-2 micro-benchmark and a stale $impl-sharing TODO.

Jacob Zhong and others added 17 commits June 16, 2026 20:27
Estimate capacity from the number's length (logarithmic in the length
ratio, since each successive power is built by repeated squaring) and
use Vec::with_capacity instead of growing the vectors push-by-push.

Co-Authored-By: Claude <noreply@anthropic.com>
The add_mul_dword_same_len_in_place and sub_mul_dword_same_len_in_place
kernels (the inner loop of schoolbook multiplication, and the base case
for Karatsuba/Toom-3) now dispatch at runtime to a BMI2 build on x86-64
when the CPU supports bmi2 (requires the std feature). The arithmetic is
shared verbatim between the portable and BMI2 builds via an
#[inline(always)] body, and the BMI2 wrapper is
#[target_feature(enable = "bmi2")], so LLVM lowers the widening
multiplies to the flag-free mulx and unrolls -- no hand-written
intrinsics, so the two builds cannot diverge. ~4-5% faster on the kernel
in isolation.

When the crate is already built with bmi2 (e.g. -C target-cpu=native or
x86-64-v3), the portable build itself uses mulx, so the runtime check is
compiled out and the change is a no-op for such users -- the runtime
dispatch only matters for portable baseline binaries.

Also adds a guide page (guide/src/performance.md) recommending
-C target-cpu=native for maximum performance, plus a parity test and an
ignored micro-benchmark.

Co-Authored-By: Claude <noreply@anthropic.com>
The forwarding macros already use the same 4-arg (sign, mag, sign, mag)
$impl signature -- forward_ubig_ibig_binop_to_repr just passes
lhs_sign = Positive -- so a single $impl definition is already shared
across IBig op IBig, UBig op IBig, and IBig op UBig wherever the output
type matches. The TODO is satisfied; drop it.

Co-Authored-By: Claude <noreply@anthropic.com>
Context::add/sub misrounds when a subtraction's result lands just below a
round number (opposite-sign summands, |large| - eps): the discarded low part
can have the opposite sign of the significand, which the exponent-alignment
+ repr_round_sum logic doesn't handle, so the result can be off by up to 1
ulp (and Zero/directed modes can report an impossible rounding flag).

No library behavior change in this commit; it only adds regression coverage:
- fuzz/tests/add_random.rs: rand-driven differential test (all 6 rounding
  modes, bases 2 and 10) comparing Context::add/sub against an independent
  oracle (exact sum at unlimited precision, re-rounded via with_precision).
- fuzz/Cargo.toml: add dashu-int dep and a [workspace] table so the excluded
  fuzz package builds standalone.
- float/tests/add.rs: a focused, #[ignore] reproducer with 5 concrete failing
  cases for debugging.

A correct fix was prototyped but regressed DBig add ~3x (it removed the
negligible-small shortcut for subtractions and added an expensive borrow
conversion); the fix is deferred and these tests are the regression net.

Co-Authored-By: Claude <noreply@anthropic.com>
The `bench_dword_kernel_bmi2_vs_portable` test was a scratch micro-benchmark
comparing portable vs BMI2 addmul-2 kernel throughput. The kernels it
exercised are already covered by `dword_kernel_edge_cases` and the
bmi2/portable parity tests.

Co-Authored-By: Claude <noreply@anthropic.com>
* Add optional `rand_v09` (rand 0.9) and `rand_v010` (rand 0.10) features
  mirroring `rand_v08` across dashu-int/float/ratio (plus root forwarding).
  The default `rand` feature remains `rand_v08`. The v08 source files are
  renamed to `rand_v08.rs` for parallel naming with `rand_v09.rs`/`rand_v010.rs`,
  with a `rand` re-export alias preserving the existing `dashu::*::rand` paths.
* Consolidate the standalone `add_sub_oracle` example into the fuzz
  differential test: draw random significands directly via `UniformBits`
  (no per-iteration string allocation/parsing), add a deterministic
  small-precision sweep, and use a guard-digit-tolerant oracle (accept the
  result matching rounding to p or p+1).
* Fix `Context::sub(0, b)` mis-rounding under the asymmetric directed modes
  `Up` (toward +inf) and `Down` (toward -inf): the zero-left path rounded
  `b` and then negated (`-round(b)`), but `round(-x) != -round(x)` for those
  modes, so the result could land one ULP off. The negated operand is now
  rounded directly. Add a regression test; remove the obsolete strict-
  precision borrow-rounding known-failures test.

Co-Authored-By: Claude <noreply@anthropic.com>
The rand distributions (UniformBits/Uniform01/UniformFBig/...) and their
sampling algorithms now live once in a version-agnostic `rand` module per
crate, generic over a `BitRng` trait (defined in dashu-int). The
`rand_v08`/`rand_v09`/`rand_v010` modules are reduced to private per-version
trait bindings.

- dashu-int::rand exposes the `BitRng` trait plus `bridge_v08`/`bridge_v09`/
  `bridge_v010` constructors, so float/ratio and end users can adapt any rand
  version's RNG (or implement `BitRng` directly).
- Version modules are now private; the distributions are accessed via
  `dashu_*::rand`. The common module docs live in `rand.rs`, and a runnable
  rand-0.8 example lives in `float/examples/random_fbig.rs`.

Co-Authored-By: Claude <noreply@anthropic.com>
The test module used `vec!`/`Vec` without imports (broke the no_std CI job) and
hardcoded 64-bit `Word` (`u64`/`u128`/`>> 64`/`u64::MAX`) in the addmul-2
reference + tests (broke the 16/32-bit `force_bits` CI matrix).

- Import `alloc::vec` / `alloc::vec::Vec` (matching other test modules), and gate
  the BMI2-only carry bindings so they aren't unused on non-x86_64 / no_std.
- Make `add_mul_dword_ref` and the dword-kernel tests `Word`-generic: shift by
  `Word::BITS`, use `Word::MAX`, and cast the PRNG output `as Word`.

Co-Authored-By: Claude <noreply@anthropic.com>
…6-bit Word

`gen_ubig` took `num_words: u16` and pushed `i.into()`; on a 16-bit Word
target (Word = u16) that is a useless u16 -> u16 conversion. Widen the
parameter to `usize` and cast the loop index `as Word`, so the conversion is
a genuine cross-type cast at every Word width (and `as Word` can't trip
`unnecessary_cast`, since `usize` is never the same type as `Word`).

Co-Authored-By: Claude <noreply@anthropic.com>
The section describing the precision of each distribution (Uniform01 /
builtin Standard* / UniformFBig) was dropped during the rand-module
de-dup. Add it back to the version-agnostic rand.rs core, keeping the
[Uniform01]/[UniformFBig]/[DoubleWord]/[FBig] intra-doc links and rendering
the version-specific `Standard`/`StandardUniform` and `Uniform` as plain text
(the core is version-agnostic).

Co-Authored-By: Claude <noreply@anthropic.com>
Remove `dword_kernel_edge_cases` (trivial all-ones/zero-multiplier cases
already covered by the two fuzz tests), and replace the hand-rolled
SplitMix64 PRNG (`next_rand`) with `rand_v08`'s `StdRng`, dropping the
manual helper and the now-unused `alloc::vec` macro import.

Co-Authored-By: Claude <noreply@anthropic.com>
Several rand `Distribution::sample` impls were missing `#[inline]`
(`UniformBits`/`UniformBelow` for integers, `UniformFBig`/`Uniform01`/
`Standard` for floats, `Uniform01<Repr>` for rationals) while others
(`Open01`, `OpenClosed01`, the rational builtins) already had it. Add it
consistently across all three rand versions so the delegating `sample`
methods inline into the caller.

Co-Authored-By: Claude <noreply@anthropic.com>
The section describing the denominator of each distribution (Uniform01 /
builtin Standard* / UniformRBig, plus the closed-interval off-by-one note)
was dropped during the rand-module de-dup. Add it back to the
version-agnostic rand.rs core, rendering the version-specific
`Standard`/`StandardUniform` and `DoubleWord::MAX` as plain text.

Co-Authored-By: Claude <noreply@anthropic.com>
rand 0.10 requires Rust 1.85 and its feature table (getrandom as an
optional dep with the `dep:` syntax) breaks the resolver under the 1.68
build, even though only `rand` (== rand_v08) is enabled there. Extend
`drop_incompatible_deps_for_msrv.py` to also remove `rand_v09`/`rand_v010`
(their deps + feature lines) from the manifests for the MSRV build, so
only rand 0.8 is resolved. rand_v09/v010 remain covered by the stable and
1.85 `--all-features` jobs.

Co-Authored-By: Claude <noreply@anthropic.com>
@cmpute
cmpute merged commit 2e4a382 into master Jun 21, 2026
13 checks passed
@cmpute
cmpute deleted the misc-optim branch June 21, 2026 07:25
CokieMiner pushed a commit to CokieMiner/dashu that referenced this pull request Jun 25, 2026
…rt (cmpute#81)

* Preallocate radix_powers/big_chunks in non-power-of-2 radix formatting

Estimate capacity from the number's length (logarithmic in the length
ratio, since each successive power is built by repeated squaring) and
use Vec::with_capacity instead of growing the vectors push-by-push.

Co-Authored-By: Claude <noreply@anthropic.com>

* Use BMI2 mulx in the basecase addmul/submul-2 kernels

The add_mul_dword_same_len_in_place and sub_mul_dword_same_len_in_place
kernels (the inner loop of schoolbook multiplication, and the base case
for Karatsuba/Toom-3) now dispatch at runtime to a BMI2 build on x86-64
when the CPU supports bmi2 (requires the std feature). The arithmetic is
shared verbatim between the portable and BMI2 builds via an
multiplies to the flag-free mulx and unrolls -- no hand-written
intrinsics, so the two builds cannot diverge. ~4-5% faster on the kernel
in isolation.

When the crate is already built with bmi2 (e.g. -C target-cpu=native or
x86-64-v3), the portable build itself uses mulx, so the runtime check is
compiled out and the change is a no-op for such users -- the runtime
dispatch only matters for portable baseline binaries.

Also adds a guide page (guide/src/performance.md) recommending
-C target-cpu=native for maximum performance, plus a parity test and an
ignored micro-benchmark.

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove stale TODO about sharing $impl between ibig/ubig binops

The forwarding macros already use the same 4-arg (sign, mag, sign, mag)
$impl signature -- forward_ubig_ibig_binop_to_repr just passes
lhs_sign = Positive -- so a single $impl definition is already shared
across IBig op IBig, UBig op IBig, and IBig op UBig wherever the output
type matches. The TODO is satisfied; drop it.

Co-Authored-By: Claude <noreply@anthropic.com>

* Add tests documenting FBig add/sub borrow-rounding bug

Context::add/sub misrounds when a subtraction's result lands just below a
round number (opposite-sign summands, |large| - eps): the discarded low part
can have the opposite sign of the significand, which the exponent-alignment
+ repr_round_sum logic doesn't handle, so the result can be off by up to 1
ulp (and Zero/directed modes can report an impossible rounding flag).

No library behavior change in this commit; it only adds regression coverage:
- fuzz/tests/add_random.rs: rand-driven differential test (all 6 rounding
  modes, bases 2 and 10) comparing Context::add/sub against an independent
  oracle (exact sum at unlimited precision, re-rounded via with_precision).
- fuzz/Cargo.toml: add dashu-int dep and a [workspace] table so the excluded
  fuzz package builds standalone.
- float/tests/add.rs: a focused, #[ignore] reproducer with 5 concrete failing
  cases for debugging.

A correct fix was prototyped but regressed DBig add ~3x (it removed the
negligible-small shortcut for subtractions and added an expensive borrow
conversion); the fix is deferred and these tests are the regression net.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix cancellation by do full subtraction

* Fix bugs

* Remove unused BMI2 addmul-2 micro-benchmark

The `bench_dword_kernel_bmi2_vs_portable` test was a scratch micro-benchmark
comparing portable vs BMI2 addmul-2 kernel throughput. The kernels it
exercised are already covered by `dword_kernel_edge_cases` and the
bmi2/portable parity tests.

Co-Authored-By: Claude <noreply@anthropic.com>

* Add rand_v09/v010 support; consolidate add/sub differential test

* Add optional `rand_v09` (rand 0.9) and `rand_v010` (rand 0.10) features
  mirroring `rand_v08` across dashu-int/float/ratio (plus root forwarding).
  The default `rand` feature remains `rand_v08`. The v08 source files are
  renamed to `rand_v08.rs` for parallel naming with `rand_v09.rs`/`rand_v010.rs`,
  with a `rand` re-export alias preserving the existing `dashu::*::rand` paths.
* Consolidate the standalone `add_sub_oracle` example into the fuzz
  differential test: draw random significands directly via `UniformBits`
  (no per-iteration string allocation/parsing), add a deterministic
  small-precision sweep, and use a guard-digit-tolerant oracle (accept the
  result matching rounding to p or p+1).
* Fix `Context::sub(0, b)` mis-rounding under the asymmetric directed modes
  `Up` (toward +inf) and `Down` (toward -inf): the zero-left path rounded
  `b` and then negated (`-round(b)`), but `round(-x) != -round(x)` for those
  modes, so the result could land one ULP off. The negated operand is now
  rounded directly. Add a regression test; remove the obsolete strict-
  precision borrow-rounding known-failures test.

Co-Authored-By: Claude <noreply@anthropic.com>

* De-duplicate rand modules: core in rand.rs, private version modules

The rand distributions (UniformBits/Uniform01/UniformFBig/...) and their
sampling algorithms now live once in a version-agnostic `rand` module per
crate, generic over a `BitRng` trait (defined in dashu-int). The
`rand_v08`/`rand_v09`/`rand_v010` modules are reduced to private per-version
trait bindings.

- dashu-int::rand exposes the `BitRng` trait plus `bridge_v08`/`bridge_v09`/
  `bridge_v010` constructors, so float/ratio and end users can adapt any rand
  version's RNG (or implement `BitRng` directly).
- Version modules are now private; the distributions are accessed via
  `dashu_*::rand`. The common module docs live in `rand.rs`, and a runnable
  rand-0.8 example lives in `float/examples/random_fbig.rs`.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix mul::simple tests for no_std and non-64-bit Word targets

The test module used `vec!`/`Vec` without imports (broke the no_std CI job) and
hardcoded 64-bit `Word` (`u64`/`u128`/`>> 64`/`u64::MAX`) in the addmul-2
reference + tests (broke the 16/32-bit `force_bits` CI matrix).

- Import `alloc::vec` / `alloc::vec::Vec` (matching other test modules), and gate
  the BMI2-only carry bindings so they aren't unused on non-x86_64 / no_std.
- Make `add_mul_dword_ref` and the dword-kernel tests `Word`-generic: shift by
  `Word::BITS`, use `Word::MAX`, and cast the PRNG output `as Word`.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix pre-existing clippy::useless_conversion in ubig buffer tests on 16-bit Word

`gen_ubig` took `num_words: u16` and pushed `i.into()`; on a 16-bit Word
target (Word = u16) that is a useless u16 -> u16 conversion. Widen the
parameter to `usize` and cast the loop index `as Word`, so the conversion is
a genuine cross-type cast at every Word width (and `as Word` can't trip
`unnecessary_cast`, since `usize` is never the same type as `Word`).

Co-Authored-By: Claude <noreply@anthropic.com>

* Restore "Precision and rounding" section to dashu-float rand module docs

The section describing the precision of each distribution (Uniform01 /
builtin Standard* / UniformFBig) was dropped during the rand-module
de-dup. Add it back to the version-agnostic rand.rs core, keeping the
[Uniform01]/[UniformFBig]/[DoubleWord]/[FBig] intra-doc links and rendering
the version-specific `Standard`/`StandardUniform` and `Uniform` as plain text
(the core is version-agnostic).

Co-Authored-By: Claude <noreply@anthropic.com>

* Simplify mul::simple dword tests: drop edge-cases, use rand

Remove `dword_kernel_edge_cases` (trivial all-ones/zero-multiplier cases
already covered by the two fuzz tests), and replace the hand-rolled
SplitMix64 PRNG (`next_rand`) with `rand_v08`'s `StdRng`, dropping the
manual helper and the now-unused `alloc::vec` macro import.

Co-Authored-By: Claude <noreply@anthropic.com>

* Add #[inline] to all Distribution::sample impls

Several rand `Distribution::sample` impls were missing `#[inline]`
(`UniformBits`/`UniformBelow` for integers, `UniformFBig`/`Uniform01`/
`Standard` for floats, `Uniform01<Repr>` for rationals) while others
(`Open01`, `OpenClosed01`, the rational builtins) already had it. Add it
consistently across all three rand versions so the delegating `sample`
methods inline into the caller.

Co-Authored-By: Claude <noreply@anthropic.com>

* Restore Denominator section to dashu-rational rand module docs

The section describing the denominator of each distribution (Uniform01 /
builtin Standard* / UniformRBig, plus the closed-interval off-by-one note)
was dropped during the rand-module de-dup. Add it back to the
version-agnostic rand.rs core, rendering the version-specific
`Standard`/`StandardUniform` and `DoubleWord::MAX` as plain text.

Co-Authored-By: Claude <noreply@anthropic.com>

* Strip rand_v09/v010 from the MSRV (1.68) build

rand 0.10 requires Rust 1.85 and its feature table (getrandom as an
optional dep with the `dep:` syntax) breaks the resolver under the 1.68
build, even though only `rand` (== rand_v08) is enabled there. Extend
`drop_incompatible_deps_for_msrv.py` to also remove `rand_v09`/`rand_v010`
(their deps + feature lines) from the manifests for the MSRV build, so
only rand 0.8 is resolved. rand_v09/v010 remain covered by the stable and
1.85 `--all-features` jobs.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Jacob Zhong <jacob@rimbot.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant