Skip to content

Commit 73f18b6

Browse files
Faster floor_sqrt, checked_sqrt implementation (#1312)
This re-applies the changes in #1258 with fixes for #1303. Changes to the `BoxedUint` proptests in #1310 also help to validate the results. The sqrt logic is implemented on `UintRef`, meaning one version is used for both `Uint` and `BoxedUint`. This uses algorithm 1.12 in Modern Computer Arithmetic rather than 1.13, which scales better for large integers (approx O(2n)). Addition and subtraction methods in `UintRef` are adjusted to accept an rhs smaller than self. This was inconsistently applied before. `checked_sqrt`(_vartime) no longer performs an additional squaring, so its performance matches `floor_sqrt`. Performance seems to be even better now, maybe due to `bitlen` inlining or other changes. <details> <summary>Sample benchmarks</summary> ``` sqrt/floor_sqrt, U256 time: [136.87 ns 137.28 ns 137.74 ns] change: [−79.375% −79.240% −79.049%] (p = 0.00 < 0.05) Performance has improved. sqrt/floor_sqrt, U512 time: [297.86 ns 298.67 ns 299.63 ns] change: [−77.808% −77.716% −77.608%] (p = 0.00 < 0.05) Performance has improved. sqrt/floor_sqrt_vartime, U256 time: [67.951 ns 68.141 ns 68.360 ns] change: [−82.235% −82.158% −82.067%] (p = 0.00 < 0.05) Performance has improved. sqrt/floor_sqrt, one Limb time: [45.111 ns 45.260 ns 45.440 ns] change: [−68.637% −68.525% −68.397%] (p = 0.00 < 0.05) Performance has improved. sqrt/floor_sqrt_vartime, U256 one Limb time: [6.7108 ns 6.7672 ns 6.8396 ns] change: [+17.539% +20.940% +23.453%] (p = 0.00 < 0.05) Performance has regressed. sqrt/checked_sqrt, U256 time: [141.94 ns 142.42 ns 142.98 ns] change: [−78.972% −78.899% −78.816%] (p = 0.00 < 0.05) Performance has improved. sqrt/checked_sqrt_vartime, U256 time: [68.770 ns 69.016 ns 69.347 ns] change: [−82.197% −82.130% −82.049%] (p = 0.00 < 0.05) Performance has improved. boxed_sqrt/floor_sqrt, 4096 time: [2.4790 µs 2.4855 µs 2.4945 µs] change: [−95.591% −95.581% −95.568%] (p = 0.00 < 0.05) Performance has improved. boxed_sqrt/floor_sqrt_vartime, 4096 time: [2.2306 µs 2.2339 µs 2.2376 µs] change: [−89.510% −89.493% −89.477%] (p = 0.00 < 0.05) Performance has improved. boxed_sqrt/checked_sqrt, 4096 time: [2.4750 µs 2.4780 µs 2.4814 µs] change: [−95.636% −95.628% −95.620%] (p = 0.00 < 0.05) Performance has improved. boxed_sqrt/checked_sqrt_vartime, 4096 time: [2.2298 µs 2.2323 µs 2.2351 µs] change: [−89.754% −89.732% −89.713%] (p = 0.00 < 0.05) Performance has improved. ``` </details> --------- Signed-off-by: Andrew Whitehead <cywolf@gmail.com>
1 parent 1ceecf0 commit 73f18b6

9 files changed

Lines changed: 641 additions & 233 deletions

File tree

‎benches/uint.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,14 @@ fn bench_sqrt(c: &mut Criterion) {
10451045
);
10461046
});
10471047

1048+
group.bench_function("floor_sqrt, one Limb", |b| {
1049+
b.iter_batched(
1050+
|| Uint::new([Limb::random_from_rng(&mut rng)]),
1051+
|x| x.floor_sqrt(),
1052+
BatchSize::SmallInput,
1053+
);
1054+
});
1055+
10481056
group.bench_function("floor_sqrt_vartime, U256 one Limb", |b| {
10491057
b.iter_batched(
10501058
|| U256::from_word(Limb::random_from_rng(&mut rng).0),

‎src/uint.rs‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub(crate) use ref_type::UintRef;
88
use crate::{
99
Bounded, Choice, ConstOne, ConstZero, Constants, CtEq, CtOption, EncodedUint, FixedInteger,
1010
Int, Integer, Limb, NonZero, Odd, One, Unsigned, UnsignedWithMontyForm, Word, Zero, bitlen,
11-
limb::nlimbs, modular::FixedMontyForm, primitives, traits::sealed::Sealed,
11+
limb::nlimbs, modular::FixedMontyForm, traits::sealed::Sealed,
1212
};
1313
use core::fmt;
1414

@@ -108,9 +108,6 @@ impl<const LIMBS: usize> Uint<LIMBS> {
108108
/// Total size of the represented integer in bits.
109109
pub const BITS: u32 = bitlen::from_limbs(LIMBS);
110110

111-
/// `floor(log2(Self::BITS))`.
112-
pub(crate) const LOG2_BITS: u32 = primitives::u32_bits(Self::BITS) - 1;
113-
114111
/// Total size of the represented integer in bytes.
115112
pub const BYTES: usize = LIMBS * Limb::BYTES;
116113

‎src/uint/boxed/sqrt.rs‎

Lines changed: 92 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
//! [`BoxedUint`] square root operations.
22
3-
use crate::{
4-
BitOps, BoxedUint, CheckedSquareRoot, ConcatenatingSquare, CtAssign, CtEq, CtGt, CtOption,
5-
FloorSquareRoot, Limb,
6-
};
7-
use core::mem;
3+
use crate::{BoxedUint, CheckedSquareRoot, Choice, CtOption, FloorSquareRoot, NonZero};
84

95
impl BoxedUint {
106
/// Computes `floor(√(self))` in constant time.
@@ -18,46 +14,13 @@ impl BoxedUint {
1814

1915
/// Computes √(`self`) in constant time.
2016
///
21-
/// Callers can check if `self` is a square by squaring the result.
17+
/// Callers can check if `self` is a square by squaring the result, or use
18+
/// `checked_sqrt`.
2219
#[must_use]
2320
pub fn floor_sqrt(&self) -> Self {
24-
// Uses Brent & Zimmermann, Modern Computer Arithmetic, v0.5.9, Algorithm 1.13.
25-
//
26-
// See Hast, "Note on computation of integer square roots"
27-
// for the proof of the sufficiency of the bound on iterations.
28-
// https://github.com/RustCrypto/crypto-bigint/files/12600669/ct_sqrt.pdf
29-
30-
// The initial guess: `x_0 = 2^ceil(b/2)`, where `2^(b-1) <= self < b`.
31-
// Will not overflow since `b <= BITS`.
32-
let mut x = Self::one_with_precision(self.bits_precision());
33-
x.unbounded_shl_assign_vartime((self.bits() + 1) >> 1); // ≥ √(`self`)
34-
35-
let mut nz_x = x.clone();
36-
let mut quo = Self::zero_with_precision(self.bits_precision());
37-
let mut rem = Self::zero_with_precision(self.bits_precision());
38-
let mut i = 0;
39-
40-
// Repeat enough times to guarantee result has stabilized.
41-
// TODO (#378): the tests indicate that just `Self::LOG2_BITS` may be enough.
42-
while i < self.log2_bits() + 2 {
43-
let x_nonzero = x.is_nonzero();
44-
nz_x.ct_assign(&x, x_nonzero);
45-
46-
// Calculate `x_{i+1} = floor((x_i + self / x_i) / 2)`
47-
quo.limbs.copy_from_slice(&self.limbs);
48-
rem.limbs.copy_from_slice(&nz_x.limbs);
49-
quo.as_mut_uint_ref().div_rem(rem.as_mut_uint_ref());
50-
x.conditional_carrying_add_assign(&quo, x_nonzero);
51-
x.shr1_assign();
52-
53-
i += 1;
54-
}
55-
56-
// At this point `x_prev == x_{n}` and `x == x_{n+1}`
57-
// where `n == i - 1 == LOG2_BITS + 1 == floor(log2(BITS)) + 1`.
58-
// Thus, according to Hast, `sqrt(self) = min(x_n, x_{n+1})`.
59-
x.ct_assign(&nz_x, x.ct_gt(&nz_x));
60-
x
21+
let mut root = self.clone();
22+
root.floor_sqrt_assign();
23+
root
6124
}
6225

6326
/// Computes `floor(√(self))`.
@@ -73,47 +36,15 @@ impl BoxedUint {
7336

7437
/// Computes √(`self`).
7538
///
76-
/// Callers can check if `self` is a square by squaring the result.
39+
/// Callers can check if `self` is a square by squaring the result, or use
40+
/// `checked_sqrt_vartime`.
7741
///
7842
/// Variable time with respect to `self`.
7943
#[must_use]
8044
pub fn floor_sqrt_vartime(&self) -> Self {
81-
// Uses Brent & Zimmermann, Modern Computer Arithmetic, v0.5.9, Algorithm 1.13
82-
83-
if self.is_zero_vartime() {
84-
return Self::zero_with_precision(self.bits_precision());
85-
}
86-
87-
// The initial guess: `x_0 = 2^ceil(b/2)`, where `2^(b-1) <= self < b`.
88-
// Will not overflow since `b <= BITS`.
89-
// The initial value of `x` is always greater than zero.
90-
let mut x = Self::one_with_precision(self.bits_precision());
91-
x.unbounded_shl_assign_vartime((self.bits() + 1) >> 1); // ≥ √(`self`)
92-
93-
let mut quo = Self::zero_with_precision(self.bits_precision());
94-
let mut rem = Self::zero_with_precision(self.bits_precision());
95-
96-
loop {
97-
// Calculate `x_{i+1} = floor((x_i + self / x_i) / 2)`
98-
quo.limbs.copy_from_slice(&self.limbs);
99-
rem.limbs.copy_from_slice(&x.limbs);
100-
quo.as_mut_uint_ref().div_rem_vartime(rem.as_mut_uint_ref());
101-
quo.carrying_add_assign(&x, Limb::ZERO);
102-
quo.shr1_assign();
103-
104-
// If `quo` is the same as `x` or greater, we reached convergence
105-
// (`x` is guaranteed to either go down or oscillate between
106-
// `sqrt(self)` and `sqrt(self) + 1`)
107-
if !x.cmp_vartime(&quo).is_gt() {
108-
break;
109-
}
110-
x.limbs.copy_from_slice(&quo.limbs);
111-
if x.is_zero_vartime() {
112-
break;
113-
}
114-
}
115-
116-
x
45+
let mut root = self.clone();
46+
root.floor_sqrt_assign_vartime();
47+
root
11748
}
11849

11950
/// Wrapped sqrt is just `floor(√(self))`.
@@ -138,9 +69,9 @@ impl BoxedUint {
13869
/// only if the square root is exact.
13970
#[must_use]
14071
pub fn checked_sqrt(&self) -> CtOption<Self> {
141-
let r = self.floor_sqrt();
142-
let s = r.wrapping_square();
143-
CtOption::new(r, self.ct_eq(&s))
72+
let mut root = self.clone();
73+
let exact = root.floor_sqrt_assign();
74+
CtOption::new(root, exact)
14475
}
14576

14677
/// Perform checked sqrt, returning an [`Option`] which `is_some`
@@ -149,24 +80,32 @@ impl BoxedUint {
14980
/// Variable time with respect to `self`.
15081
#[must_use]
15182
pub fn checked_sqrt_vartime(&self) -> Option<Self> {
152-
let r = self.floor_sqrt_vartime();
153-
let s = r.wrapping_square();
154-
if self.cmp_vartime(&s).is_eq() {
155-
Some(r)
83+
let mut root = self.clone();
84+
if root.floor_sqrt_assign_vartime() {
85+
Some(root)
15686
} else {
15787
None
15888
}
15989
}
16090

91+
/// Assigns `floor(√(self))` to `self` in constant time, and returns a [`Choice`]
92+
/// indicating whether the square root is exact.
93+
fn floor_sqrt_assign(&mut self) -> Choice {
94+
let size = self.nlimbs();
95+
let mut buf = Self::zero_with_precision(self.bits_precision() * 2);
96+
self.as_mut_uint_ref()
97+
.sqrt_assign(buf.as_mut_uint_ref().split_at_mut(size))
98+
}
99+
161100
/// Assigns `floor(√(self))` to `self`, and returns a [`bool`]
162101
/// indicating whether the square root is exact.
163102
///
164103
/// Variable time with respect to `self`.
165104
pub fn floor_sqrt_assign_vartime(&mut self) -> bool {
166-
// TODO(tarcieri): more optimized implementation
167-
let mut ret = self.floor_sqrt_vartime();
168-
mem::swap(&mut ret, self);
169-
self.concatenating_square() == ret
105+
let size = self.nlimbs();
106+
let mut buf = Self::zero_with_precision(self.bits_precision() * 2);
107+
self.as_mut_uint_ref()
108+
.sqrt_assign_vartime(buf.as_mut_uint_ref().split_at_mut(size))
170109
}
171110
}
172111

@@ -192,14 +131,38 @@ impl FloorSquareRoot for BoxedUint {
192131
}
193132
}
194133

134+
impl CheckedSquareRoot for NonZero<BoxedUint> {
135+
type Output = Self;
136+
137+
fn checked_sqrt(&self) -> CtOption<Self> {
138+
self.as_ref().checked_sqrt().map(NonZero::new_unchecked)
139+
}
140+
141+
fn checked_sqrt_vartime(&self) -> Option<Self> {
142+
self.as_ref()
143+
.checked_sqrt_vartime()
144+
.map(NonZero::new_unchecked)
145+
}
146+
}
147+
148+
impl FloorSquareRoot for NonZero<BoxedUint> {
149+
fn floor_sqrt(&self) -> Self {
150+
NonZero::new_unchecked(self.as_ref().floor_sqrt())
151+
}
152+
153+
fn floor_sqrt_vartime(&self) -> Self {
154+
NonZero::new_unchecked(self.as_ref().floor_sqrt_vartime())
155+
}
156+
}
157+
195158
#[cfg(test)]
196159
#[allow(clippy::integer_division_remainder_used, reason = "test")]
197160
mod tests {
198-
use crate::{BoxedUint, Limb};
161+
use crate::{BoxedUint, CheckedSquareRoot, FloorSquareRoot, Limb};
199162

200163
#[cfg(feature = "rand_core")]
201164
use {
202-
crate::RandomBits,
165+
crate::{ConcatenatingSquare, RandomBits},
203166
chacha20::ChaCha8Rng,
204167
rand_core::{Rng, SeedableRng},
205168
};
@@ -218,7 +181,7 @@ mod tests {
218181
for i in 0..half.limbs.len() / 2 {
219182
half.limbs[i] = Limb::MAX;
220183
}
221-
let u256_max = !BoxedUint::zero_with_precision(256);
184+
let u256_max = BoxedUint::max(256);
222185
assert_eq!(u256_max.floor_sqrt(), half);
223186

224187
// Test edge cases that use up the maximum number of iterations.
@@ -307,8 +270,26 @@ mod tests {
307270
let r = BoxedUint::from(*e);
308271
assert_eq!(l.floor_sqrt(), r);
309272
assert_eq!(l.floor_sqrt_vartime(), r);
310-
assert!(l.checked_sqrt().is_some().to_bool());
311-
assert!(l.checked_sqrt_vartime().is_some());
273+
assert_eq!(
274+
CheckedSquareRoot::checked_sqrt(&l).into_option().as_ref(),
275+
Some(&r)
276+
);
277+
assert_eq!(
278+
CheckedSquareRoot::checked_sqrt_vartime(&l).as_ref(),
279+
Some(&r)
280+
);
281+
let nz_l = l.as_nz_vartime().unwrap();
282+
let nz_r = r.to_nz().unwrap();
283+
assert_eq!(FloorSquareRoot::floor_sqrt(nz_l), nz_r);
284+
assert_eq!(FloorSquareRoot::floor_sqrt_vartime(nz_l), nz_r);
285+
assert_eq!(
286+
CheckedSquareRoot::checked_sqrt(nz_l).into_option().as_ref(),
287+
Some(&nz_r)
288+
);
289+
assert_eq!(
290+
CheckedSquareRoot::checked_sqrt_vartime(nz_l).as_ref(),
291+
Some(&nz_r)
292+
);
312293
}
313294
}
314295

@@ -362,8 +343,6 @@ mod tests {
362343
#[cfg(feature = "rand_core")]
363344
#[test]
364345
fn fuzz() {
365-
use crate::{CheckedSquareRoot, ConcatenatingSquare};
366-
367346
let mut rng = ChaCha8Rng::from_seed([7u8; 32]);
368347
let rounds = if cfg!(miri) { 10 } else { 50 };
369348
for _ in 0..rounds {
@@ -384,4 +363,19 @@ mod tests {
384363
assert_eq!(s.concatenating_square().floor_sqrt_vartime(), s2);
385364
}
386365
}
366+
367+
#[test]
368+
// test input from issue #1303
369+
fn sqrt_edge() {
370+
let sq = BoxedUint::from_be_hex(
371+
"0000000000000AB88E41D05334F646EE1B715C614C81DCE1C46E2E021EF2E7FA45E5DD2CC670725E52B091A0E08B1E9E6C43262581226A7BFDE534F704D6DC6C1FD5C93B3AE4FB73C9D26EBC09DB83040C2CD157884532EB5578CEF2BA95E87C7F6E328F062BB20D3C95D122EB4D26727901A424AF14143EBCEFE5D5958E9EDEA73649F5C76932D491C4A51C370F51E6DBEB66D56D7FB51A",
372+
1216,
373+
).unwrap();
374+
let expect = BoxedUint::from_be_hex(
375+
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000346375360711FDFC13BE3CF276FCC535C2E29164A842D3B71E2C7DFA60AE0E24CD0CC65AD7A3B626E03DAC8260D921AB8E16C6757D2CA8DEE5DFA62E5C1CB3C90A49B9F1A192498905",
376+
1216,
377+
).unwrap();
378+
assert_eq!(sq.floor_sqrt(), expect);
379+
assert_eq!(sq.floor_sqrt_vartime(), expect);
380+
}
387381
}

‎src/uint/ref_type.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod mul;
1010
mod shl;
1111
mod shr;
1212
mod slice;
13+
mod sqrt;
1314
mod sub;
1415

1516
use crate::{Choice, Limb, NonZero, Odd, Uint, Word};

0 commit comments

Comments
 (0)