Skip to content

Minor improve on division and implement Montgomery reducer - #77

Merged
cmpute merged 11 commits into
masterfrom
div-improve
Jun 17, 2026
Merged

Minor improve on division and implement Montgomery reducer#77
cmpute merged 11 commits into
masterfrom
div-improve

Conversation

@cmpute

@cmpute cmpute commented Jun 16, 2026

Copy link
Copy Markdown
Owner

No description provided.

Jacob Zhong and others added 11 commits June 16, 2026 11:18
Mirror the multiplication threshold env-var mechanism for division: when the `tuning` feature is active, `DASHU_THRESHOLD_SIMPLE_DIV` overrides the compile-time schoolbook / divide-and-conquer crossover. Values below 3 are unsupported (the divide-and-conquer split needs at least 2 low words).

Co-Authored-By: Claude <noreply@anthropic.com>
Implement an optimized Montgomery modular arithmetic suite under integer/src/monty/, mirroring the structure of integer/src/modular/. Two new public types: MontgomeryRepr (precomputed Montgomery constants for an odd modulus, analogous to ConstDivisor) and Montgomery<'a> (a value in Montgomery form, analogous to Reduced), with multiplication, squaring, addition, subtraction, negation, doubling, exponentiation, and inversion.

Single/double-word moduli delegate to num-modular's Montgomery<Word>/<DoubleWord>. The multi-word (Large) path uses word-aligned REDC: the operand product reuses the crate's fast multiply (Karatsuba/Toom-3/NTT), and the reduction is word-by-word with a per-word n0 = -m^-1 mod 2^WORD_BITS (computed by table-free Hensel lifting). Values are kept in the canonical range [0, m).

Cross-validated against the Barrett-style modular::Reduced across Single/Double/Large moduli (integer/tests/monty.rs).

Co-Authored-By: Claude <noreply@anthropic.com>
Switch the multi-word Montgomery reduction from single-word (one limb/iteration) to double-word (two limbs/iteration via the existing addmul_2 kernel, n0_dword = -m^-1 mod 2^(2*WORD_BITS)), which halves modulus memory traffic. The exit path (residue/inv, sparse input) keeps the single-word REDC, which measures faster there. Microbenchmarks show mul/sqr/pow 10-19% faster than single-word, and inv now faster than either all-single or all-double.

Also add integer/benches/modular.rs comparing the Barrett (ConstDivisor) and Montgomery backends across modular mul/sqr/add/sub/pow/inv and modulus sizes (256..16384 bits).

Co-Authored-By: Claude <noreply@anthropic.com>
Drop the single-word n0 field (derive it on demand as the low word of n0_dword via n0_word()) and the r_mod_m field (compute the Montgomery form of 1 on demand as REDC(R^2 mod m) in mul::one_large). This leaves the ring storing only modulus, n0_dword, and r2_mod_m — the minimal set needed for REDC and entering Montgomery form.

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

Add a 'When to use Montgomery vs Barrett' section to the monty module docs and a Performance note on Montgomery::inv, explaining that Montgomery wins for mul/sqr/pow (~256-4096 bits) but Barrett (modular::Reduced) is substantially faster for inverses, since a Montgomery inverse must exit Montgomery form, run ext-GCD, and re-enter. Also corrects the module doc (word-by-word REDC with the addmul_2 kernel, not 'Almost-Montgomery' or fast-multiply correction) and updates the changelog entry to match.

Co-Authored-By: Claude <noreply@anthropic.com>
Only 'Efficient Software Implementations of Modular Exponentiation' (Gueron) is relevant to this module; the 'Incomplete Reduction' (Yanik/Savaş/Koç) reference is removed. The Gueron paper now lives as a normal // XXX code comment in pow.rs pointing to AMM as a possible future optimization, rather than as module-level documentation.

Co-Authored-By: Claude <noreply@anthropic.com>
…pointer-identity in mul

- sqr() and pow_nontrivial: build the squared result directly from scratch
  memory via sqr_normalized_large instead of cloning raw then overwriting,
  saving one Box<[Word]> allocation and an s-word copy per squaring.
- mul_in_place_large: replace O(n) Box<[Word]> value comparison with O(1)
  pointer-identity check (lhs.0.as_ptr() == rhs.0.as_ptr()) to detect
  self-multiplication (a *= a). Distinct-but-equal values are handled
  correctly by the general mul path.
- Extract finish_monty_product helper (REDC + canonicalize) shared by
  mul_normalized_large and sqr_normalized_large.

fix(div): clamp DASHU_THRESHOLD_SIMPLE_DIV to minimum of 3

- The divide-and-conquer algorithm requires n_lo >= 2, which implies a
  threshold >= 3. The env-var override now enforces this floor instead
  of relying solely on the compile-time const_assert! on the default.
- Added explicit ::<usize> type annotation on parse() to disambiguate
  the v.max(3) call.

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

The &Montgomery * &Montgomery impl previously cloned self then used
mul_assign, which completely overwrites the cloned buffer via
copy_from_slice from scratch memory.  For Large (multi-word) operands
this wasted one Box<[Word]> allocation and an s-word copy.

Now the Large case builds the result directly from scratch via
mul_normalized_large (or sqr_normalized_large when lhs and rhs share
the same allocation), saving one allocation and copy per multiply.

Single/Double (Copy types) keep the existing clone path — it is
zero-cost for them.

Add, sub, and neg on references are intentionally unchanged: their
in-place operations read-then-write lhs.0, so the clone correctly
seeds the output buffer with the operand value.

Co-Authored-By: Claude <noreply@anthropic.com>
- Add forward_modular_binop_to_assign!, impl_modular_commutative_op_for_ref!,
  impl_modular_binop_ref_ref_by_clone!, forward_modular_binop_to_ref_ref! macros
  in helper_macros.rs, parameterized by target type. Apply them to both Montgomery
  and Reduced Add/Sub/Mul/Div, eliminating the by-value/by-ref/assign boilerplate
  per operator.
- Move add_mul_word_same_len_in_place, add_mul_word_in_place,
  sub_mul_word_same_len_in_place from mul/mod.rs to mul/simple.rs (alongside the
  other schoolbook kernels), and widen add_mul_dword_same_len_in_place from
  pub(crate) to pub. Old mul::* paths still work via re-exports.
- Extract simple::MIN_LEN = 3 in div/simple.rs (mirroring mul::karatsuba::MIN_LEN);
  the threshold tuning override now clamps against this named constant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cmpute
cmpute merged commit a15420b into master Jun 17, 2026
13 checks passed
@cmpute
cmpute deleted the div-improve branch June 17, 2026 16:20
CokieMiner pushed a commit to CokieMiner/dashu that referenced this pull request Jun 25, 2026
* Add DASHU_THRESHOLD_SIMPLE_DIV runtime override for division threshold

Mirror the multiplication threshold env-var mechanism for division: when the `tuning` feature is active, `DASHU_THRESHOLD_SIMPLE_DIV` overrides the compile-time schoolbook / divide-and-conquer crossover. Values below 3 are unsupported (the divide-and-conquer split needs at least 2 low words).

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

* Add Montgomery modular arithmetic (monty module)

Implement an optimized Montgomery modular arithmetic suite under integer/src/monty/, mirroring the structure of integer/src/modular/. Two new public types: MontgomeryRepr (precomputed Montgomery constants for an odd modulus, analogous to ConstDivisor) and Montgomery<'a> (a value in Montgomery form, analogous to Reduced), with multiplication, squaring, addition, subtraction, negation, doubling, exponentiation, and inversion.

Single/double-word moduli delegate to num-modular's Montgomery<Word>/<DoubleWord>. The multi-word (Large) path uses word-aligned REDC: the operand product reuses the crate's fast multiply (Karatsuba/Toom-3/NTT), and the reduction is word-by-word with a per-word n0 = -m^-1 mod 2^WORD_BITS (computed by table-free Hensel lifting). Values are kept in the canonical range [0, m).

Cross-validated against the Barrett-style modular::Reduced across Single/Double/Large moduli (integer/tests/monty.rs).

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

* Use double-word Montgomery REDC and add modular benchmark

Switch the multi-word Montgomery reduction from single-word (one limb/iteration) to double-word (two limbs/iteration via the existing addmul_2 kernel, n0_dword = -m^-1 mod 2^(2*WORD_BITS)), which halves modulus memory traffic. The exit path (residue/inv, sparse input) keeps the single-word REDC, which measures faster there. Microbenchmarks show mul/sqr/pow 10-19% faster than single-word, and inv now faster than either all-single or all-double.

Also add integer/benches/modular.rs comparing the Barrett (ConstDivisor) and Montgomery backends across modular mul/sqr/add/sub/pow/inv and modulus sizes (256..16384 bits).

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

* Eliminate redundant n0 and r_mod_m fields from MontgomeryLargeRepr

Drop the single-word n0 field (derive it on demand as the low word of n0_dword via n0_word()) and the r_mod_m field (compute the Montgomery form of 1 on demand as REDC(R^2 mod m) in mul::one_large). This leaves the ring storing only modulus, n0_dword, and r2_mod_m — the minimal set needed for REDC and entering Montgomery form.

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

* Document Montgomery vs Barrett tradeoff (prefer Barrett for inv-heavy use)

Add a 'When to use Montgomery vs Barrett' section to the monty module docs and a Performance note on Montgomery::inv, explaining that Montgomery wins for mul/sqr/pow (~256-4096 bits) but Barrett (modular::Reduced) is substantially faster for inverses, since a Montgomery inverse must exit Montgomery form, run ext-GCD, and re-enter. Also corrects the module doc (word-by-word REDC with the addmul_2 kernel, not 'Almost-Montgomery' or fast-multiply correction) and updates the changelog entry to match.

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

* Drop the j56 reference; move the Gueron paper to a code comment

Only 'Efficient Software Implementations of Modular Exponentiation' (Gueron) is relevant to this module; the 'Incomplete Reduction' (Yanik/Savaş/Koç) reference is removed. The Gueron paper now lives as a normal // XXX code comment in pow.rs pointing to AMM as a possible future optimization, rather than as module-level documentation.

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

* perf(monty): eliminate redundant clone+overwrite in sqr and pow, use pointer-identity in mul

- sqr() and pow_nontrivial: build the squared result directly from scratch
  memory via sqr_normalized_large instead of cloning raw then overwriting,
  saving one Box<[Word]> allocation and an s-word copy per squaring.
- mul_in_place_large: replace O(n) Box<[Word]> value comparison with O(1)
  pointer-identity check (lhs.0.as_ptr() == rhs.0.as_ptr()) to detect
  self-multiplication (a *= a). Distinct-but-equal values are handled
  correctly by the general mul path.
- Extract finish_monty_product helper (REDC + canonicalize) shared by
  mul_normalized_large and sqr_normalized_large.

fix(div): clamp DASHU_THRESHOLD_SIMPLE_DIV to minimum of 3

- The divide-and-conquer algorithm requires n_lo >= 2, which implies a
  threshold >= 3. The env-var override now enforces this floor instead
  of relying solely on the compile-time const_assert! on the default.
- Added explicit ::<usize> type annotation on parse() to disambiguate
  the v.max(3) call.

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

* perf(monty): eliminate clone in &Montgomery * &Montgomery for Large operands

The &Montgomery * &Montgomery impl previously cloned self then used
mul_assign, which completely overwrites the cloned buffer via
copy_from_slice from scratch memory.  For Large (multi-word) operands
this wasted one Box<[Word]> allocation and an s-word copy.

Now the Large case builds the result directly from scratch via
mul_normalized_large (or sqr_normalized_large when lhs and rhs share
the same allocation), saving one allocation and copy per multiply.

Single/Double (Copy types) keep the existing clone path — it is
zero-cost for them.

Add, sub, and neg on references are intentionally unchanged: their
in-place operations read-then-write lhs.0, so the clone correctly
seeds the output buffer with the operand value.

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

* refactor(int): dedup modular binop impls and reorganize mul/div helpers

- Add forward_modular_binop_to_assign!, impl_modular_commutative_op_for_ref!,
  impl_modular_binop_ref_ref_by_clone!, forward_modular_binop_to_ref_ref! macros
  in helper_macros.rs, parameterized by target type. Apply them to both Montgomery
  and Reduced Add/Sub/Mul/Div, eliminating the by-value/by-ref/assign boilerplate
  per operator.
- Move add_mul_word_same_len_in_place, add_mul_word_in_place,
  sub_mul_word_same_len_in_place from mul/mod.rs to mul/simple.rs (alongside the
  other schoolbook kernels), and widen add_mul_dword_same_len_in_place from
  pub(crate) to pub. Old mul::* paths still work via re-exports.
- Extract simple::MIN_LEN = 3 in div/simple.rs (mirroring mul::karatsuba::MIN_LEN);
  the threshold tuning override now clamps against this named constant.

Co-Authored-By: Claude Opus 4.7 <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