Implement number theoretic transform for large integer multiplication - #282
Implement number theoretic transform for large integer multiplication#282byeongkeunahn wants to merge 1 commit into
Conversation
|
I want to first acknowledge this with thanks -- it's quite impressive to be near GMP performance! However to set expectations: this is also a large PR, and I will need some studying to understand what's going on, so it may take me a while to find time to review this. |
eae5b13 to
66faa24
Compare
|
Thanks. I'd like to note a few changes in the latest commit, which brings 10-15% performance gains and reduced memory footprint:
|
2639746 to
0e41192
Compare
|
Does the chart shows that current algorithm is faster than GMP? That's impressive. |
|
I ran benchmark fib_hex 100m from https://github.com/tczajka/bigint-benchmark-rs on this PR and it made num-bigint twice faster than malachite, slightly faster than gmp and 12x faster than itself. |
|
Obviously the performance here is impressive, as I said before. However, as I finally started to try reviewing this, I have a few high level objections.
(that particular one does pass under tree-borrows though) |
|
Thanks for your review and comments! I’ll try to address the issues, including removing the unsafe code and cleaning up the commit history, even if that comes at the cost of a small performance regression. Integrating the Montgomery reduction code with
|
|
We can treat the monty-consolidation as "nice to have". However, any specific tailoring needs comments, lest that work be undone by a later contributor or maintainer. I'm skeptical that those optimizations wouldn't be useful to the other monty use as well though... |
|
I’ve made the suggested improvements:
I also made a small improvement to the planner based on exhaustive Criterion.rs benchmarks covering operand sizes of up to 300 million bits each. This reduced execution time by 1.2% on average. Some of this work was completed with assistance from ChatGPT Codex. Thanks again for the detailed review. Please let me know if there are any remaining issues. |
| #![allow(clippy::too_many_arguments)] | ||
| #![allow(clippy::similar_names)] | ||
|
|
||
| use crate::biguint::Vec; |
There was a problem hiding this comment.
It's odd to use an absolute path when it's not an intended re-export.
| use crate::biguint::Vec; | |
| use super::Vec; |
| } | ||
| // Modular inverse: a^-1 mod modulus |
There was a problem hiding this comment.
Style nit here and throughout: please add a blank line between functions/consts/etc.
Even within code blocks, if there's a comment explaining the following code, then also add a blank line before. (except if it's the first part at a new indentation level)
| for m5 in 0..=Arith::<P>::factors(5) { | ||
| for m3 in 0..=Arith::<P>::factors(3) { |
There was a problem hiding this comment.
Might it be worth forcing const-evaluation on these? i.e. using a const { ... } block, or perhaps adding Arith::FACTORS_5 etc.
| break; | ||
| } | ||
| let (mut len, mut m2) = (len as usize, 0); | ||
| while len < min_len && m2 < Arith::<P>::factors(2) { |
There was a problem hiding this comment.
Also FWIW, factors(2) is just (P-1).trailing_zeros() -- but it doesn't really matter either way if we're doing it at compile time.
| let (mut tmp, mut cost) = (len, 0); | ||
| let mut g_new = 1; | ||
|
|
||
| // Length-dependent weights for cost estimation. |
There was a problem hiding this comment.
These weights and cost formulas look like a whole lot of "magic" constants. I suppose they come from your PR statement:
The NTT length is selected by exhaustively evaluating cost estimates for all allowed lengths within a factor of two.
It would be good to have comments directly in the code to explain this, ideally with some instructions how one might reevaluate these costs. If there's any third-party source as well, please link to that.
| #![allow(clippy::many_single_char_names)] | ||
| #![allow(clippy::needless_range_loop)] | ||
| #![allow(clippy::too_many_arguments)] | ||
| #![allow(clippy::similar_names)] |
There was a problem hiding this comment.
I'm not interested in maintaining a bunch of pedantic clippy suppressions. AFAICS, the only lint here that we hit by default is too_many_arguments, and that's only on ntt5_kernel and ntt6_kernel, so let's just allow specifically on those.
There was a problem hiding this comment.
For too-many-args, it might make sense to switch those to arrays, e.g.
const fn ntt5_kernel<const P: u64, const INV: bool, const TWIDDLE: bool>(
[w1, w2, w3, w4]: [u64; 4],
[a, mut b, mut c, mut d, mut e]: [u64; 5],
) -> [u64; 5] {... but I haven't checked whether that affects benchmarks.
| assert!(!x.is_empty() && x.len() == y.len()); | ||
| let (_n, g, m, last_radix) = (plan.n, plan.g, plan.m, plan.last_radix as u64); | ||
|
|
||
| /* multiply by a constant in advance */ |
There was a problem hiding this comment.
Another style nit: please use // comments throughout, rather than /* */
| // Propagates carry from the beginning to the end of acc, | ||
| // and returns the resulting carry if it is nonzero. |
There was a problem hiding this comment.
| // Propagates carry from the beginning to the end of acc, | |
| // and returns the resulting carry if it is nonzero. | |
| // Propagates carry from the beginning to the end of acc, | |
| // and returns the resulting carry if it is nonzero. |
| // Computes c as u128 * mreduce(v) as u128, | ||
| // using d: u64 = mmulmod(P-1, c). |
There was a problem hiding this comment.
| // Computes c as u128 * mreduce(v) as u128, | |
| // using d: u64 = mmulmod(P-1, c). | |
| // Computes c as u128 * mreduce(v) as u128, using d: u64 = mmulmod(P-1, c). | |
| // |
| // Process remaining carries. The addition carry_acc + bitbuf should not overflow | ||
| // since bitbuf is underfilled and carry_acc is always 0 or 1. |
There was a problem hiding this comment.
| // Process remaining carries. The addition carry_acc + bitbuf should not overflow | |
| // since bitbuf is underfilled and carry_acc is always 0 or 1. | |
| // Process remaining carries. The addition carry_acc + bitbuf should not overflow | |
| // since bitbuf is underfilled and carry_acc is always 0 or 1. |
This commit implements number theoretic transform (NTT) for large integer multiplication (issue #169).
On Ryzen 7 2700X, 64bit, it takes about 15ms for 2.7Mbits x 2.7Mbits and 170ms for 27Mbits x 27Mbits multiplication. This seems comparable to GMP 6.2.1.