Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up i256 division and remainder operations #4303

Merged
merged 11 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions arrow-buffer/benches/i256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,23 @@

use arrow_buffer::i256;
use criterion::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use std::str::FromStr;

/// Returns fixed seedable RNG
fn seedable_rng() -> StdRng {
StdRng::seed_from_u64(42)
}

fn create_i256_vec(size: usize) -> Vec<i256> {
let mut rng = seedable_rng();

(0..size)
.map(|_| i256::from_i128(rng.gen::<i128>()))
.collect()
}

fn criterion_benchmark(c: &mut Criterion) {
let numbers = vec![
i256::ZERO,
Expand All @@ -38,6 +53,28 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| i256::from_str(&t).unwrap());
});
}

c.bench_function("i256_div", |b| {
b.iter(|| {
for number_a in create_i256_vec(10) {
for number_b in create_i256_vec(5) {
number_a.checked_div(number_b);
number_a.wrapping_div(number_b);
}
}
});
});

c.bench_function("i256_rem", |b| {
b.iter(|| {
for number_a in create_i256_vec(10) {
for number_b in create_i256_vec(5) {
number_a.checked_rem(number_b);
number_a.wrapping_rem(number_b);
}
}
});
});
}

criterion_group!(benches, criterion_benchmark);
Expand Down
167 changes: 137 additions & 30 deletions arrow-buffer/src/bigint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,33 @@ use std::num::ParseIntError;
use std::ops::{BitAnd, BitOr, BitXor, Neg, Shl, Shr};
use std::str::FromStr;

/// An opaque error similar to [`std::num::ParseIntError`]
/// [`i256`] operations return this error type.
#[derive(Debug)]
pub struct ParseI256Error {}
pub enum I256Error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change (which is fine)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've filed #4318 to remove this breaking change, PTAL

/// An opaque error similar to [`std::num::ParseIntError`]
ParseError,
/// Division by zero
DivideByZero,
/// Division overflow
DivideOverflow,
}

impl From<ParseIntError> for ParseI256Error {
impl From<ParseIntError> for I256Error {
fn from(_: ParseIntError) -> Self {
Self {}
I256Error::ParseError
}
}

impl std::fmt::Display for ParseI256Error {
impl std::fmt::Display for I256Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to parse as i256")
match self {
I256Error::ParseError => write!(f, "Failed to parse as i256"),
I256Error::DivideByZero => write!(f, "Division by zero"),
I256Error::DivideOverflow => write!(f, "Division overflow"),
}
}
}
impl std::error::Error for ParseI256Error {}
impl std::error::Error for I256Error {}

/// A signed 256-bit integer
#[allow(non_camel_case_types)]
Expand All @@ -60,7 +71,7 @@ impl std::fmt::Display for i256 {
}

impl FromStr for i256 {
type Err = ParseI256Error;
type Err = I256Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// i128 can store up to 38 decimal digits
Expand All @@ -82,15 +93,15 @@ impl FromStr for i256 {

if !s.as_bytes()[0].is_ascii_digit() {
// Ensures no duplicate sign
return Err(ParseI256Error {});
return Err(I256Error::ParseError);
}

parse_impl(s, negative)
}
}

/// Parse `s` with any sign and leading 0s removed
fn parse_impl(s: &str, negative: bool) -> Result<i256, ParseI256Error> {
fn parse_impl(s: &str, negative: bool) -> Result<i256, I256Error> {
if s.len() <= 38 {
let low = i128::from_str(s)?;
return Ok(match negative {
Expand All @@ -102,7 +113,7 @@ fn parse_impl(s: &str, negative: bool) -> Result<i256, ParseI256Error> {
let split = s.len() - 38;
if !s.as_bytes()[split].is_ascii_digit() {
// Ensures not splitting codepoint and no sign
return Err(ParseI256Error {});
return Err(I256Error::ParseError);
}
let (hs, ls) = s.split_at(split);

Expand All @@ -117,7 +128,7 @@ fn parse_impl(s: &str, negative: bool) -> Result<i256, ParseI256Error> {

high.checked_mul(i256::from_i128(10_i128.pow(38)))
.and_then(|high| high.checked_add(low))
.ok_or(ParseI256Error {})
.ok_or(I256Error::ParseError)
}

impl PartialOrd for i256 {
Expand Down Expand Up @@ -396,42 +407,101 @@ impl i256 {
.then_some(Self { low, high })
}

/// Return the least number of bits needed to represent the number
#[inline]
fn bits_required(&self) -> usize {
let le_bytes = self.to_le_bytes();
let arr: [u128; 2] = [
u128::from_le_bytes(le_bytes[0..16].try_into().unwrap()),
u128::from_le_bytes(le_bytes[16..32].try_into().unwrap()),
];

let iter = arr.iter().rev().take(2 - 1);
if self.is_negative() {
let ctr = iter.take_while(|&&b| b == ::core::u128::MAX).count();
(128 * (2 - ctr)) + 1 - (!arr[2 - ctr - 1]).leading_zeros() as usize
} else {
let ctr = iter.take_while(|&&b| b == ::core::u128::MIN).count();
(128 * (2 - ctr)) + 1 - arr[2 - ctr - 1].leading_zeros() as usize
}
}

/// Division operation, returns (quotient, remainder).
/// This basically implements [Long division]: `<https://en.wikipedia.org/wiki/Division_algorithm>`
#[inline]
fn div_rem(self, other: Self) -> Result<(Self, Self), I256Error> {
if other == Self::ZERO {
return Err(I256Error::DivideByZero);
}
if other == Self::MINUS_ONE && self == Self::MIN {
return Err(I256Error::DivideOverflow);
}

if self == Self::MIN || other == Self::MIN {
let l = BigInt::from_signed_bytes_le(&self.to_le_bytes());
let r = BigInt::from_signed_bytes_le(&other.to_le_bytes());
let d = i256::from_bigint_with_overflow(&l / &r).0;
tustvold marked this conversation as resolved.
Show resolved Hide resolved
let r = i256::from_bigint_with_overflow(&l % &r).0;
return Ok((d, r));
}

let mut me = self.checked_abs().unwrap();
let mut you = other.checked_abs().unwrap();
let mut ret = [0u128; 2];
if me < you {
return Ok((Self::from_parts(ret[0], ret[1] as i128), self));
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if at this point we could fallback to 128-bit division if both the high components are zero?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The low component is u128. If the high component is zero, it might be possibly over i128 range, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a problem, we have already taken the absolute value, so it is an unsigned division operation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, got your idea. You meant to do it in u128.

let shift = me.bits_required() - you.bits_required();
viirya marked this conversation as resolved.
Show resolved Hide resolved
you = you.shl(shift as u8);
for i in (0..=shift).rev() {
if me >= you {
ret[i / 128] |= 1 << (i % 128);
me = me.checked_sub(you).unwrap();
}
you = you.shr(1);
}

Ok((
if self.is_negative() == other.is_negative() {
Self::from_parts(ret[0], ret[1] as i128)
} else {
-Self::from_parts(ret[0], ret[1] as i128)
},
if self.is_negative() { -me } else { me },
))
}

/// Performs wrapping division
#[inline]
pub fn wrapping_div(self, other: Self) -> Self {
let l = BigInt::from_signed_bytes_le(&self.to_le_bytes());
let r = BigInt::from_signed_bytes_le(&other.to_le_bytes());
Self::from_bigint_with_overflow(l / r).0
match self.div_rem(other) {
Ok((v, _)) => v,
Err(I256Error::DivideByZero) => panic!("attempt to divide by zero"),
Err(_) => Self::MIN,
}
}

/// Performs checked division
#[inline]
pub fn checked_div(self, other: Self) -> Option<Self> {
let l = BigInt::from_signed_bytes_le(&self.to_le_bytes());
let r = BigInt::from_signed_bytes_le(&other.to_le_bytes());
let (val, overflow) = Self::from_bigint_with_overflow(l / r);
(!overflow).then_some(val)
self.div_rem(other).map(|(v, _)| v).ok()
}

/// Performs wrapping remainder
#[inline]
pub fn wrapping_rem(self, other: Self) -> Self {
let l = BigInt::from_signed_bytes_le(&self.to_le_bytes());
let r = BigInt::from_signed_bytes_le(&other.to_le_bytes());
Self::from_bigint_with_overflow(l % r).0
match self.div_rem(other) {
Ok((_, v)) => v,
Err(I256Error::DivideByZero) => panic!("attempt to divide by zero"),
Err(_) => Self::ZERO,
}
}

/// Performs checked remainder
#[inline]
pub fn checked_rem(self, other: Self) -> Option<Self> {
if other == Self::ZERO {
return None;
}

let l = BigInt::from_signed_bytes_le(&self.to_le_bytes());
let r = BigInt::from_signed_bytes_le(&other.to_le_bytes());
let (val, overflow) = Self::from_bigint_with_overflow(l % r);
(!overflow).then_some(val)
self.div_rem(other).map(|(_, v)| v).ok()
}

/// Performs checked exponentiation
Expand Down Expand Up @@ -853,6 +923,43 @@ mod tests {
),
}

// Division
if ir != i256::ZERO {
let actual = il.wrapping_div(ir);
let expected = bl.clone() / br.clone();
let checked = il.checked_div(ir);

if ir == i256::MINUS_ONE && il == i256::MIN {
// BigInt produces an integer over i256::MAX
assert_eq!(actual, i256::MIN);
assert!(checked.is_none());
} else {
assert_eq!(actual.to_string(), expected.to_string());
assert_eq!(checked.unwrap().to_string(), expected.to_string());
}
} else {
// `wrapping_div` panics on division by zero
assert!(il.checked_div(ir).is_none());
}

// Remainder
if ir != i256::ZERO {
let actual = il.wrapping_rem(ir);
let expected = bl.clone() % br.clone();
let checked = il.checked_rem(ir);

assert_eq!(actual.to_string(), expected.to_string());

if ir == i256::MINUS_ONE && il == i256::MIN {
assert!(checked.is_none());
} else {
assert_eq!(checked.unwrap().to_string(), expected.to_string());
}
} else {
// `wrapping_rem` panics on division by zero
assert!(il.checked_rem(ir).is_none());
}

// Exponentiation
for exp in vec![0, 1, 2, 3, 8, 100].into_iter() {
let actual = il.wrapping_pow(exp);
Expand Down