From 1889166c49de104ebaa6a1217467077a7489fd99 Mon Sep 17 00:00:00 2001 From: Petros Angelatos Date: Tue, 29 Apr 2025 16:12:23 +0300 Subject: [PATCH 1/4] add decimal parse benchmark --- dec/benches/dec.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/dec/benches/dec.rs b/dec/benches/dec.rs index 65eb84e..70361da 100644 --- a/dec/benches/dec.rs +++ b/dec/benches/dec.rs @@ -15,7 +15,7 @@ use std::convert::TryFrom; -use criterion::{criterion_group, criterion_main, Bencher, Criterion}; +use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::{thread_rng, Rng}; use dec::{Context, Decimal, Decimal128, Decimal64}; @@ -42,6 +42,17 @@ pub fn bench_decode(c: &mut Criterion) { c.bench_function("decode_decimal128", |b| bench_decode_decimal128(d128, b)); } +pub fn bench_parse(c: &mut Criterion) { + let mut cx = Context::>::default(); + c.bench_function("parse_decimal", |b| { + b.iter(|| { + black_box(cx.from_f64(black_box(f64::MIN))); + black_box(cx.from_f64(black_box(f64::from_bits(0x8008000000000000)))); + black_box(cx.from_f64(black_box(f64::MAX))); + }) + }); +} + pub fn bench_to_string(d: Decimal128, b: &mut Bencher) { b.iter_with_setup( || { @@ -177,5 +188,11 @@ pub fn bench_tryinto_primitive(c: &mut Criterion) { }); } -criterion_group!(benches, bench_decode, bench_print, bench_tryinto_primitive); +criterion_group!( + benches, + bench_decode, + bench_print, + bench_tryinto_primitive, + bench_parse +); criterion_main!(benches); From c1498b00de8572dbb63a5059bde92a40883cc565 Mon Sep 17 00:00:00 2001 From: Petros Angelatos Date: Wed, 23 Apr 2025 21:26:43 +0300 Subject: [PATCH 2/4] add parsing variant that doesn't allocate Signed-off-by: Petros Angelatos --- dec/src/decimal.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dec/src/decimal.rs b/dec/src/decimal.rs index 5334d27..d06909f 100644 --- a/dec/src/decimal.rs +++ b/dec/src/decimal.rs @@ -1400,13 +1400,18 @@ impl Context> { where S: Into>, { - validate_n(N); let c_string = CString::new(s).map_err(|_| ParseDecimalError)?; + self.parse_c_str(c_string.as_c_str()) + } + + /// Parses a number from its string representation. + pub fn parse_c_str(&mut self, s: &CStr) -> Result, ParseDecimalError> { + validate_n(N); let mut d = Decimal::zero(); unsafe { decnumber_sys::decNumberFromString( d.as_mut_ptr() as *mut decnumber_sys::decNumber, - c_string.as_ptr(), + s.as_ptr(), &mut self.inner, ); }; From 2af10a812ebe2253482280df183147c7ae453680 Mon Sep 17 00:00:00 2001 From: Petros Angelatos Date: Tue, 29 Apr 2025 15:50:29 +0300 Subject: [PATCH 3/4] optimize float to decimal conversion Signed-off-by: Petros Angelatos --- dec/src/decimal.rs | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/dec/src/decimal.rs b/dec/src/decimal.rs index d06909f..7eeeacf 100644 --- a/dec/src/decimal.rs +++ b/dec/src/decimal.rs @@ -16,7 +16,8 @@ use std::cmp::Ordering; use std::convert::{TryFrom, TryInto}; use std::ffi::{CStr, CString}; -use std::fmt; +use std::fmt::{self, LowerExp}; +use std::io::Write; use std::iter::{Product, Sum}; use std::marker::PhantomData; use std::mem::MaybeUninit; @@ -1749,7 +1750,7 @@ impl Context> { /// Both of these are guaranteed to fit comfortably within `Decimal`'s /// constraints. pub fn from_f32(&mut self, n: f32) -> Decimal { - self.parse(n.to_string().as_str()).unwrap() + self.from_float(n) } /// Converts an `f64` to a `Decimal`. @@ -1761,7 +1762,45 @@ impl Context> { /// Both of these are guaranteed to fit comfortably within `Decimal`'s /// constraints. pub fn from_f64(&mut self, n: f64) -> Decimal { - self.parse(n.to_string().as_str()).unwrap() + self.from_float(n) + } + + /// Converts a `f32` or a `f64` value to a `Decimal`. + /// + /// Note that this conversion is infallible because `f64`'s: + /// - Maximum precision is ~18 + /// - Min/max exponent is ~ -305, 305 + /// + /// Both of these are guaranteed to fit comfortably within `Decimal`'s + /// constraints. + // NOTE: The code is generic over any T: LowerExp but passing something like f128 wouldn't + // always work since there are f128 values that don't fit in 24 bytes. + pub fn from_float(&mut self, n: T) -> Decimal { + // The maximum bytes needed to store the decimal representation of a f64. + // This is because you have at most: + // * 1 byte for a possible leading negative sign + // * 17 bytes of significant digits + // See: https://stdrs.dev/nightly/x86_64-pc-windows-gnu/core/num/flt2dec/constant.MAX_SIG_DIGITS.html + // * 1 byte for a decimal point + // * 2 bytes for a negative exponent (e-) + // * 3 bytes for the largest possible exponent (308) + // An example of such maximal float value is f64::from_bits(0x8008000000000000) whose + // decimal representation is '-1.1125369292536007e-308' + const MAX_LEN: usize = 24; + + // Create a buffer that can hold the longest float plus a nul character for the C string + let mut buf = [0u8; MAX_LEN + 1]; + let mut unwritten = &mut buf[..MAX_LEN]; + write!(unwritten, "{:e}", n).unwrap(); + let unwritten_len = unwritten.len(); + // SAFETY: + // * buf was zero-initialized + // * Exactly MAX_LEN - unwritten.len() bytes have been modified + // * Formatting a float never writes nul characters + // Therefore the produced slice contains exactly one nul character + let c_buf = + unsafe { CStr::from_bytes_with_nul_unchecked(&buf[..MAX_LEN - unwritten_len + 1]) }; + self.parse_c_str(c_buf).unwrap() } /// Computes the digitwise logical inversion of `n`, storing the result in From c7db9dad67a9dfe69f401d13850aa7ff37cffd87 Mon Sep 17 00:00:00 2001 From: Petros Angelatos Date: Tue, 29 Apr 2025 21:29:05 +0300 Subject: [PATCH 4/4] update ctest Signed-off-by: Petros Angelatos --- systest/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systest/Cargo.toml b/systest/Cargo.toml index cf36726..bf7a48e 100644 --- a/systest/Cargo.toml +++ b/systest/Cargo.toml @@ -10,4 +10,4 @@ decnumber-sys = { path = "../decnumber-sys" } libc = "0.2.68" [build-dependencies] -ctest = "0.2" +ctest = "0.4"