libm: Improved integer utilities, implement shifts and bug fixes for i256 and u256

`i256` and `u256`
- operators now use the same overflow convention as primitives
- implement `<<` and `-` (previously just `>>` and `+`)
- implement `Ord` correctly (the previous `PartialOrd` was broken)
- correct `i256::SIGNED` to `true`

The `Int`-trait is extended with `trailing_zeros`, `carrying_add`, and
`borrowing_sub`.
This commit is contained in:
quaternic 2025-07-01 11:07:48 +03:00 committed by GitHub
parent a4d584e7a6
commit 6c4221818e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 229 additions and 66 deletions

View file

@ -119,6 +119,22 @@ fn icount_bench_u256_add(cases: Vec<(u256, u256)>) {
}
}
#[library_benchmark]
#[bench::linspace(setup_u256_add())]
fn icount_bench_u256_sub(cases: Vec<(u256, u256)>) {
for (x, y) in cases.iter().copied() {
black_box(black_box(x) - black_box(y));
}
}
#[library_benchmark]
#[bench::linspace(setup_u256_shift())]
fn icount_bench_u256_shl(cases: Vec<(u256, u32)>) {
for (x, y) in cases.iter().copied() {
black_box(black_box(x) << black_box(y));
}
}
#[library_benchmark]
#[bench::linspace(setup_u256_shift())]
fn icount_bench_u256_shr(cases: Vec<(u256, u32)>) {
@ -129,7 +145,7 @@ fn icount_bench_u256_shr(cases: Vec<(u256, u32)>) {
library_benchmark_group!(
name = icount_bench_u128_group;
benchmarks = icount_bench_u128_widen_mul, icount_bench_u256_add, icount_bench_u256_shr
benchmarks = icount_bench_u128_widen_mul, icount_bench_u256_add, icount_bench_u256_sub, icount_bench_u256_shl, icount_bench_u256_shr
);
#[library_benchmark]

View file

@ -111,12 +111,54 @@ fn mp_u256_add() {
let y = random_u256(&mut rng);
assign_bigint(&mut bx, x);
assign_bigint(&mut by, y);
let actual = x + y;
let actual = if u256::MAX - x >= y {
x + y
} else {
// otherwise (u256::MAX - x) < y, so the wrapped result is
// (x + y) - (u256::MAX + 1) == y - (u256::MAX - x) - 1
y - (u256::MAX - x) - 1_u128.widen()
};
bx += &by;
check_one(|| hexu(x), || Some(hexu(y)), actual, &mut bx);
}
}
#[test]
fn mp_u256_sub() {
let mut rng = ChaCha8Rng::from_seed(*SEED);
let mut bx = BigInt::new();
let mut by = BigInt::new();
for _ in 0..bigint_fuzz_iteration_count() {
let x = random_u256(&mut rng);
let y = random_u256(&mut rng);
assign_bigint(&mut bx, x);
assign_bigint(&mut by, y);
// since the operators (may) panic on overflow,
// we should test something that doesn't
let actual = if x >= y { x - y } else { y - x };
bx -= &by;
bx.abs_mut();
check_one(|| hexu(x), || Some(hexu(y)), actual, &mut bx);
}
}
#[test]
fn mp_u256_shl() {
let mut rng = ChaCha8Rng::from_seed(*SEED);
let mut bx = BigInt::new();
for _ in 0..bigint_fuzz_iteration_count() {
let x = random_u256(&mut rng);
let shift: u32 = rng.random_range(0..256);
assign_bigint(&mut bx, x);
let actual = x << shift;
bx <<= shift;
check_one(|| hexu(x), || Some(shift.to_string()), actual, &mut bx);
}
}
#[test]
fn mp_u256_shr() {
let mut rng = ChaCha8Rng::from_seed(*SEED);
@ -124,7 +166,7 @@ fn mp_u256_shr() {
for _ in 0..bigint_fuzz_iteration_count() {
let x = random_u256(&mut rng);
let shift: u32 = rng.random_range(0..255);
let shift: u32 = rng.random_range(0..256);
assign_bigint(&mut bx, x);
let actual = x >> shift;
bx >>= shift;

View file

@ -11,10 +11,10 @@ const U128_LO_MASK: u128 = u64::MAX as u128;
/// A 256-bit unsigned integer represented as two 128-bit native-endian limbs.
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub struct u256 {
pub lo: u128,
pub hi: u128,
pub lo: u128,
}
impl u256 {
@ -28,17 +28,17 @@ impl u256 {
pub fn signed(self) -> i256 {
i256 {
lo: self.lo,
hi: self.hi,
hi: self.hi as i128,
}
}
}
/// A 256-bit signed integer represented as two 128-bit native-endian limbs.
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub struct i256 {
pub hi: i128,
pub lo: u128,
pub hi: u128,
}
impl i256 {
@ -47,7 +47,7 @@ impl i256 {
pub fn unsigned(self) -> u256 {
u256 {
lo: self.lo,
hi: self.hi,
hi: self.hi as u128,
}
}
}
@ -73,17 +73,17 @@ impl MinInt for i256 {
type Unsigned = u256;
const SIGNED: bool = false;
const SIGNED: bool = true;
const BITS: u32 = 256;
const ZERO: Self = Self { lo: 0, hi: 0 };
const ONE: Self = Self { lo: 1, hi: 0 };
const MIN: Self = Self {
lo: 0,
hi: 1 << 127,
lo: u128::MIN,
hi: i128::MIN,
};
const MAX: Self = Self {
lo: u128::MAX,
hi: u128::MAX >> 1,
hi: i128::MAX,
};
}
@ -109,11 +109,78 @@ macro_rules! impl_common {
}
}
impl ops::Add<Self> for $ty {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let (lo, carry) = self.lo.overflowing_add(rhs.lo);
let (hi, of) = Int::carrying_add(self.hi, rhs.hi, carry);
debug_assert!(!of, "attempt to add with overflow");
Self { lo, hi }
}
}
impl ops::Sub<Self> for $ty {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
let (lo, borrow) = self.lo.overflowing_sub(rhs.lo);
let (hi, of) = Int::borrowing_sub(self.hi, rhs.hi, borrow);
debug_assert!(!of, "attempt to subtract with overflow");
Self { lo, hi }
}
}
impl ops::Shl<u32> for $ty {
type Output = Self;
fn shl(self, _rhs: u32) -> Self::Output {
unimplemented!("only used to meet trait bounds")
fn shl(mut self, rhs: u32) -> Self::Output {
debug_assert!(rhs < Self::BITS, "attempt to shift left with overflow");
let half_bits = Self::BITS / 2;
let low_mask = half_bits - 1;
let s = rhs & low_mask;
let lo = self.lo;
let hi = self.hi;
self.lo = lo << s;
if rhs & half_bits == 0 {
self.hi = (lo >> (low_mask ^ s) >> 1) as _;
self.hi |= hi << s;
} else {
self.hi = self.lo as _;
self.lo = 0;
}
self
}
}
impl ops::Shr<u32> for $ty {
type Output = Self;
fn shr(mut self, rhs: u32) -> Self::Output {
debug_assert!(rhs < Self::BITS, "attempt to shift right with overflow");
let half_bits = Self::BITS / 2;
let low_mask = half_bits - 1;
let s = rhs & low_mask;
let lo = self.lo;
let hi = self.hi;
self.hi = hi >> s;
#[allow(unused_comparisons)]
if rhs & half_bits == 0 {
self.lo = (hi << (low_mask ^ s) << 1) as _;
self.lo |= lo >> s;
} else {
self.lo = self.hi as _;
self.hi = if hi < 0 { !0 } else { 0 };
}
self
}
}
};
@ -122,47 +189,6 @@ macro_rules! impl_common {
impl_common!(i256);
impl_common!(u256);
impl ops::Add<Self> for u256 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let (lo, carry) = self.lo.overflowing_add(rhs.lo);
let hi = self.hi.wrapping_add(carry as u128).wrapping_add(rhs.hi);
Self { lo, hi }
}
}
impl ops::Shr<u32> for u256 {
type Output = Self;
fn shr(mut self, rhs: u32) -> Self::Output {
debug_assert!(rhs < Self::BITS, "attempted to shift right with overflow");
if rhs >= Self::BITS {
return Self::ZERO;
}
if rhs == 0 {
return self;
}
if rhs < 128 {
self.lo >>= rhs;
self.lo |= self.hi << (128 - rhs);
} else {
self.lo = self.hi >> (rhs - 128);
}
if rhs < 128 {
self.hi >>= rhs;
} else {
self.hi = 0;
}
self
}
}
impl HInt for u128 {
type D = u256;
@ -200,7 +226,7 @@ impl HInt for u128 {
}
fn widen_hi(self) -> Self::D {
self.widen() << <Self as MinInt>::BITS
u256 { lo: 0, hi: self }
}
}
@ -208,11 +234,10 @@ impl HInt for i128 {
type D = i256;
fn widen(self) -> Self::D {
let mut ret = self.unsigned().zero_widen().signed();
if self.is_negative() {
ret.hi = u128::MAX;
i256 {
lo: self as u128,
hi: if self < 0 { -1 } else { 0 },
}
ret
}
fn zero_widen(self) -> Self::D {
@ -228,7 +253,7 @@ impl HInt for i128 {
}
fn widen_hi(self) -> Self::D {
self.widen() << <Self as MinInt>::BITS
i256 { lo: 0, hi: self }
}
}
@ -252,6 +277,6 @@ impl DInt for i256 {
}
fn hi(self) -> Self::H {
self.hi as i128
self.hi
}
}

View file

@ -36,7 +36,7 @@ fn widen_i128() {
(LOHI_SPLIT as i128).widen(),
i256 {
lo: LOHI_SPLIT,
hi: u128::MAX
hi: -1,
}
);
assert_eq!((-1i128).zero_widen().unsigned(), (u128::MAX).widen());
@ -275,3 +275,64 @@ fn shr_u256_overflow() {
assert_eq!(u256::MAX >> 257, u256::ZERO);
assert_eq!(u256::MAX >> u32::MAX, u256::ZERO);
}
#[test]
fn u256_ord() {
let _1 = u256::ONE;
let _2 = _1 + _1;
for x in u8::MIN..u8::MAX {
let y = x + 1;
let wx = (x as u128).widen_hi();
let wy = (y as u128).widen_hi();
assert!([wx, wx + _1, wx + _2, wy, wy + _1, wy + _2].is_sorted());
}
}
#[test]
fn i256_ord() {
let _1 = i256::ONE;
let _2 = _1 + _1;
for x in i8::MIN..i8::MAX {
let y = x + 1;
let wx = (x as i128).widen_hi();
let wy = (y as i128).widen_hi();
assert!([wx, wx + _1, wx + _2, wy - _2, wy - _1, wy].is_sorted());
}
}
#[test]
fn u256_shifts() {
let _1 = u256::ONE;
for k in 0..255 {
let x = _1 << k;
let x2 = _1 << (k + 1);
assert!(x < x2);
assert_eq!(x << 1, x2);
assert_eq!(x + x, x2);
assert_eq!(x >> k, _1);
assert_eq!(x2 >> (k + 1), _1);
}
}
#[test]
fn i256_shifts() {
let _1 = i256::ONE;
for k in 0..254 {
let x = _1 << k;
let x2 = _1 << (k + 1);
assert!(x < x2);
assert_eq!(x << 1, x2);
assert_eq!(x + x, x2);
assert_eq!(x >> k, _1);
assert_eq!(x2 >> (k + 1), _1);
}
let min = _1 << 255;
assert_eq!(min, i256::MIN);
let mut x = min;
for k in 0..255 {
assert_eq!(x, min >> k);
let y = x >> 1;
assert_eq!(y + y, x);
assert!(x < y);
x = y;
}
}

View file

@ -37,8 +37,6 @@ pub trait Int:
+ fmt::Display
+ fmt::Binary
+ fmt::LowerHex
+ PartialEq
+ PartialOrd
+ ops::AddAssign
+ ops::SubAssign
+ ops::MulAssign
@ -102,7 +100,10 @@ pub trait Int:
fn rotate_left(self, other: u32) -> Self;
fn overflowing_add(self, other: Self) -> (Self, bool);
fn overflowing_sub(self, other: Self) -> (Self, bool);
fn carrying_add(self, other: Self, carry: bool) -> (Self, bool);
fn borrowing_sub(self, other: Self, borrow: bool) -> (Self, bool);
fn leading_zeros(self) -> u32;
fn trailing_zeros(self) -> u32;
fn ilog2(self) -> u32;
}
@ -168,12 +169,30 @@ macro_rules! int_impl_common {
<Self>::leading_zeros(self)
}
fn trailing_zeros(self) -> u32 {
<Self>::trailing_zeros(self)
}
fn ilog2(self) -> u32 {
// On our older MSRV, this resolves to the trait method. Which won't actually work,
// but this is only called behind other gates.
#[allow(clippy::incompatible_msrv)]
<Self>::ilog2(self)
}
fn carrying_add(self, other: Self, carry: bool) -> (Self, bool) {
let (ab, of1) = self.overflowing_add(other);
let (abc, of2) = ab.overflowing_add(Self::from_bool(carry));
// `of1 && of2` is possible with signed integers if a negative sum
// overflows to `MAX` and adding the carry overflows again back to `MIN`
(abc, of1 ^ of2)
}
fn borrowing_sub(self, other: Self, borrow: bool) -> (Self, bool) {
let (ab, of1) = self.overflowing_sub(other);
let (abc, of2) = ab.overflowing_sub(Self::from_bool(borrow));
(abc, of1 ^ of2)
}
};
}