Skip to content

Commit cdc7142

Browse files
authored
ctutils: add primitives::borrowing_sub_mask_u32/u64 (#1577)
It seems `crypto-bigint` has undergone a bit of a performance regression and these are the primitive needed to fix it.
1 parent 9f40823 commit cdc7142

4 files changed

Lines changed: 187 additions & 11 deletions

File tree

‎ctutils/CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
### Added
9+
- `primitives::borrowing_sub_mask_u32` and `borrowing_sub_mask_u64` for propagating
10+
word-sized borrow masks without per-limb `Choice` conversions
11+
712
## 0.4.5 (2026-10-23)
813
### Added
914
- `primitives` module ([#1570], [#1572])

‎ctutils/checkct/driver/src/driver/primitives.rs‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,72 @@ macro_rules! subtraction_checks {
145145

146146
subtraction_checks!(u32_sub, u32, borrowing_sub_u32, to_u32_mask);
147147
subtraction_checks!(u64_sub, u64, borrowing_sub_u64, to_u64_mask);
148+
149+
macro_rules! subtraction_mask_checks {
150+
($module:ident, $ty:ty, $sub:ident) => {
151+
mod $module {
152+
use super::*;
153+
154+
#[checkct]
155+
pub fn borrowing_sub_mask() {
156+
black_box(PublicRng.next_u32());
157+
black_box(primitives::$sub(
158+
private_integer!($ty),
159+
private_integer!($ty),
160+
private_integer!($ty),
161+
));
162+
}
163+
164+
#[checkct]
165+
pub fn borrowing_sub_mask_zero_borrow() {
166+
black_box(PublicRng.next_u32());
167+
black_box(primitives::$sub(
168+
private_integer!($ty),
169+
private_integer!($ty),
170+
0,
171+
));
172+
}
173+
174+
#[checkct]
175+
pub fn borrowing_sub_mask_limbs() {
176+
black_box(PublicRng.next_u32());
177+
let lhs = [private_integer!($ty), private_integer!($ty)];
178+
let rhs = [private_integer!($ty), private_integer!($ty)];
179+
let mut borrow = private_integer!($ty);
180+
let mut difference = [0; 2];
181+
for i in 0..2 {
182+
(difference[i], borrow) = primitives::$sub(lhs[i], rhs[i], borrow);
183+
}
184+
black_box((difference, borrow));
185+
}
186+
187+
/// Discarding the final borrow can change how the subtraction is optimized.
188+
#[checkct]
189+
pub fn wrapping_sub_mask_limbs() {
190+
black_box(PublicRng.next_u32());
191+
let lhs = [private_integer!($ty), private_integer!($ty)];
192+
let rhs = [private_integer!($ty), private_integer!($ty)];
193+
let mut borrow = 0;
194+
let mut difference = [0; 2];
195+
for i in 0..2 {
196+
(difference[i], borrow) = primitives::$sub(lhs[i], rhs[i], borrow);
197+
}
198+
black_box(difference);
199+
}
200+
201+
/// Keep the subtraction and masked add-back together to test their optimization.
202+
#[checkct]
203+
pub fn borrowing_sub_mask_add_back() {
204+
black_box(PublicRng.next_u32());
205+
let lhs = private_integer!($ty);
206+
let rhs = private_integer!($ty);
207+
let incoming = private_integer!($ty);
208+
let (difference, borrow) = primitives::$sub(lhs, rhs, incoming);
209+
black_box(difference.wrapping_add(rhs & borrow));
210+
}
211+
}
212+
};
213+
}
214+
215+
subtraction_mask_checks!(u32_sub_mask, u32, borrowing_sub_mask_u32);
216+
subtraction_mask_checks!(u64_sub_mask, u64, borrowing_sub_mask_u64);

‎ctutils/src/primitives.rs‎

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,27 +89,61 @@ comparison_primitives!(u32, le_u32, lt_u32, nz_u32, from_u32_lsb);
8989
comparison_primitives!(u64, le_u64, lt_u64, nz_u64, from_u64_lsb);
9090

9191
macro_rules! borrowing_sub_primitives {
92-
($ty:ty, $sub:ident, $from_lsb:ident) => {
92+
($ty:ty, $sub:ident, $sub_mask:ident, $sub_inner:ident, $from_lsb:ident) => {
9393
/// Subtract `rhs` and an incoming borrow from `lhs`.
9494
///
9595
/// Returns the wrapping difference and [`Choice::TRUE`] if the subtraction underflows.
9696
/// The incoming `borrow` subtracts one when true and zero when false.
9797
#[inline]
9898
#[must_use]
9999
pub const fn $sub(lhs: $ty, rhs: $ty, borrow: Choice) -> ($ty, Choice) {
100-
let difference = lhs.wrapping_sub(rhs).wrapping_sub(borrow.to_u8() as $ty);
100+
let (difference, borrow) = $sub_inner(lhs, rhs, borrow.to_u8() as $ty);
101+
(difference, Choice::$from_lsb(borrow))
102+
}
103+
104+
/// Subtract `rhs` and an incoming borrow from `lhs`, using word-sized borrow masks.
105+
///
106+
/// The incoming borrow is the most significant bit of `borrow`; all other bits are ignored.
107+
/// Returns the wrapping difference and an all-ones mask if the subtraction underflows,
108+
/// or zero otherwise.
109+
///
110+
/// The outgoing mask can be passed directly to the next limb's subtraction, avoiding
111+
/// conversions to and from [`Choice`] at each step of a multi-limb subtraction.
112+
#[inline]
113+
#[must_use]
114+
pub const fn $sub_mask(lhs: $ty, rhs: $ty, borrow: $ty) -> ($ty, $ty) {
115+
let (difference, borrow) = $sub_inner(lhs, rhs, borrow >> (<$ty>::BITS - 1));
116+
(difference, borrow.wrapping_neg())
117+
}
118+
119+
// The incoming borrow must be zero or one; the outgoing borrow is also zero or one.
120+
#[inline(always)]
121+
const fn $sub_inner(lhs: $ty, rhs: $ty, borrow: $ty) -> ($ty, $ty) {
122+
let difference = lhs.wrapping_sub(rhs).wrapping_sub(borrow);
101123
// Keep all arithmetic in the operand type: widening can trigger incorrect
102124
// optimization on older compilers: https://github.com/rust-lang/rust/issues/149522
103-
// Keep the full-width bit opaque before narrowing it to Choice, so a caller's
104-
// masked add-back cannot be folded into a secret-dependent branch.
125+
// Keep the full-width bit opaque before forming a mask or narrowing it to Choice,
126+
// so a caller's masked add-back cannot be folded into a secret-dependent branch.
105127
let borrow = black_box(bitborrow!(lhs, rhs, difference, <$ty>::BITS));
106-
(difference, Choice::$from_lsb(borrow))
128+
(difference, borrow)
107129
}
108130
};
109131
}
110132

111-
borrowing_sub_primitives!(u32, borrowing_sub_u32, from_u32_lsb);
112-
borrowing_sub_primitives!(u64, borrowing_sub_u64, from_u64_lsb);
133+
borrowing_sub_primitives!(
134+
u32,
135+
borrowing_sub_u32,
136+
borrowing_sub_mask_u32,
137+
borrowing_sub_inner_u32,
138+
from_u32_lsb
139+
);
140+
borrowing_sub_primitives!(
141+
u64,
142+
borrowing_sub_u64,
143+
borrowing_sub_mask_u64,
144+
borrowing_sub_inner_u64,
145+
from_u64_lsb
146+
);
113147

114148
/// Count leading zero bits, returning 32 if `value` is zero.
115149
#[inline]
@@ -276,6 +310,12 @@ mod tests {
276310
assert!(borrowing_sub_u64(0, 0, Choice::TRUE).1.to_bool());
277311
assert!(!borrowing_sub_u32(1, 0, Choice::TRUE).1.to_bool());
278312
assert!(!borrowing_sub_u64(1, 0, Choice::TRUE).1.to_bool());
313+
assert!(borrowing_sub_mask_u32(0, 0, u32::MAX).0 == u32::MAX);
314+
assert!(borrowing_sub_mask_u32(0, 0, u32::MAX).1 == u32::MAX);
315+
assert!(borrowing_sub_mask_u64(0, 0, u64::MAX).0 == u64::MAX);
316+
assert!(borrowing_sub_mask_u64(0, 0, u64::MAX).1 == u64::MAX);
317+
assert!(borrowing_sub_mask_u32(1, 0, u32::MAX).1 == 0);
318+
assert!(borrowing_sub_mask_u64(1, 0, u64::MAX).1 == 0);
279319
assert!(leading_zeros_u32(0) == 32);
280320
assert!(trailing_zeros_u32(0) == 32);
281321
assert!(leading_zeros_u64(0) == 64);
@@ -311,15 +351,17 @@ mod tests {
311351
}
312352

313353
macro_rules! subtraction_tests {
314-
($test:ident, $ty:ty, $sub:ident) => {
354+
($test:ident, $ty:ty, $sub:ident, $sub_mask:ident) => {
315355
#[test]
316356
fn $test() {
317357
let half = 1 << (<$ty>::BITS >> 1);
318358
let high = 1 << (<$ty>::BITS - 1);
319359
let values = [0, 1, half - 1, half, half + 1, high - 1, high, <$ty>::MAX];
320360
for lhs in values {
321361
for rhs in values {
322-
for incoming in [false, true] {
362+
// Include noncanonical masks: only the most significant bit matters.
363+
for mask in [0, 1, high - 1, high, <$ty>::MAX - 1, <$ty>::MAX] {
364+
let incoming = mask >> (<$ty>::BITS - 1) != 0;
323365
let (expected, first_borrow) = lhs.overflowing_sub(rhs);
324366
let (expected, second_borrow) =
325367
expected.overflowing_sub(<$ty>::from(incoming));
@@ -329,15 +371,35 @@ mod tests {
329371
(difference, borrow.to_bool()),
330372
(expected, first_borrow | second_borrow)
331373
);
374+
assert_eq!(
375+
$sub_mask(lhs, rhs, mask),
376+
(
377+
expected,
378+
<$ty>::from(first_borrow | second_borrow).wrapping_neg()
379+
)
380+
);
332381
}
333382
}
334383
}
335384
}
336385
};
337386
}
338387

339-
subtraction_tests!(sub_u32, u32, borrowing_sub_u32);
340-
subtraction_tests!(sub_u64, u64, borrowing_sub_u64);
388+
subtraction_tests!(sub_u32, u32, borrowing_sub_u32, borrowing_sub_mask_u32);
389+
subtraction_tests!(sub_u64, u64, borrowing_sub_u64, borrowing_sub_mask_u64);
390+
391+
#[test]
392+
fn sub_mask_propagates_borrow() {
393+
let (lo, borrow) = borrowing_sub_mask_u32(0, 1, 0);
394+
let (mid, borrow) = borrowing_sub_mask_u32(0, 0, borrow);
395+
assert_eq!((lo, mid, borrow), (u32::MAX, u32::MAX, u32::MAX));
396+
assert_eq!(borrowing_sub_mask_u32(1, 0, borrow), (0, 0));
397+
398+
let (lo, borrow) = borrowing_sub_mask_u64(0, 1, 0);
399+
let (mid, borrow) = borrowing_sub_mask_u64(0, 0, borrow);
400+
assert_eq!((lo, mid, borrow), (u64::MAX, u64::MAX, u64::MAX));
401+
assert_eq!(borrowing_sub_mask_u64(1, 0, borrow), (0, 0));
402+
}
341403

342404
macro_rules! addition_tests {
343405
($test:ident, $ty:ty, $add:ident) => {

‎ctutils/tests/primitives.rs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,46 @@ proptest! {
2020
prop_assert_eq!((difference, borrow.to_bool()), (expected, first_borrow | second_borrow));
2121
}
2222

23+
#[test]
24+
fn sub_mask_u32(lhs in any::<u32>(), rhs in any::<u32>(), incoming in any::<u32>()) {
25+
let (expected, first_borrow) = lhs.overflowing_sub(rhs);
26+
let (expected, second_borrow) = expected.overflowing_sub(incoming >> 31);
27+
let expected_borrow = u32::from(first_borrow | second_borrow).wrapping_neg();
28+
prop_assert_eq!(borrowing_sub_mask_u32(lhs, rhs, incoming), (expected, expected_borrow));
29+
}
30+
31+
#[test]
32+
fn sub_mask_u64(lhs in any::<u64>(), rhs in any::<u64>(), incoming in any::<u64>()) {
33+
let (expected, first_borrow) = lhs.overflowing_sub(rhs);
34+
let (expected, second_borrow) = expected.overflowing_sub(incoming >> 63);
35+
let expected_borrow = u64::from(first_borrow | second_borrow).wrapping_neg();
36+
prop_assert_eq!(borrowing_sub_mask_u64(lhs, rhs, incoming), (expected, expected_borrow));
37+
}
38+
39+
#[test]
40+
#[allow(clippy::cast_possible_truncation, reason = "extract the low limb")]
41+
fn sub_mask_u32_limbs(lhs in any::<u64>(), rhs in any::<u64>(), incoming in any::<u32>()) {
42+
let (expected, first_borrow) = lhs.overflowing_sub(rhs);
43+
let (expected, second_borrow) = expected.overflowing_sub(u64::from(incoming >> 31));
44+
let expected_borrow = u32::from(first_borrow | second_borrow).wrapping_neg();
45+
let (lo, borrow) = borrowing_sub_mask_u32(lhs as u32, rhs as u32, incoming);
46+
let (hi, borrow) = borrowing_sub_mask_u32((lhs >> 32) as u32, (rhs >> 32) as u32, borrow);
47+
let difference = (u64::from(hi) << 32) | u64::from(lo);
48+
prop_assert_eq!((difference, borrow), (expected, expected_borrow));
49+
}
50+
51+
#[test]
52+
#[allow(clippy::cast_possible_truncation, reason = "extract the low limb")]
53+
fn sub_mask_u64_limbs(lhs in any::<u128>(), rhs in any::<u128>(), incoming in any::<u64>()) {
54+
let (expected, first_borrow) = lhs.overflowing_sub(rhs);
55+
let (expected, second_borrow) = expected.overflowing_sub(u128::from(incoming >> 63));
56+
let expected_borrow = u64::from(first_borrow | second_borrow).wrapping_neg();
57+
let (lo, borrow) = borrowing_sub_mask_u64(lhs as u64, rhs as u64, incoming);
58+
let (hi, borrow) = borrowing_sub_mask_u64((lhs >> 64) as u64, (rhs >> 64) as u64, borrow);
59+
let difference = (u128::from(hi) << 64) | u128::from(lo);
60+
prop_assert_eq!((difference, borrow), (expected, expected_borrow));
61+
}
62+
2363
#[test]
2464
fn zero_counts_u32(value in any::<u32>()) {
2565
prop_assert_eq!(leading_zeros_u32(value), value.leading_zeros());

0 commit comments

Comments
 (0)