fastnum

Crates.iofastnum
lib.rsfastnum
version0.0.5
sourcesrc
created_at2024-10-21 15:16:38.276713
updated_at2024-11-06 11:20:10.834197
descriptionFast numbers library
homepagehttps://github.com/neogenie/fastnum
repositoryhttps://github.com/neogenie/fastnum
max_upload_size
id1417628
size258,859
Neo (neogenie)

documentation

https://docs.rs/fastnum

README

fastnum

Crates.io doc.rs

Fixed-size signed and unsigned integers and arbitrary precision decimal numbers implemented in pure Rust. Suitable for financial, crypto and any other fixed-precision calculations.

API Docs

Overview

This crate is inspired by num_bigint and bigdecimal - an amazing crates that allows you to store big integers and arbitrary precision fixed-point decimal numbers almost any precision.

BigInt internally uses a Vec of decimal digits the size of which is theoretically limited only by the usize max value or memory capacity.

Under the hood BigDecimal uses a BigInt object, paired with a 64-bit integer which determines the position of the decimal point. Therefore, the precision is not actually arbitrary, but limited to 2 63 decimal places.

Despite the seemingly undeniable advantages at first glance, this approach also has a number of fundamental disadvantages:

  • Non-copyable types for both integers and fixed point numbers.
  • Dynamic allocation to store even very small numbers, for example, 0 or 1.
  • Extra dynamic allocation for almost any operation (mathematical operations, parsing, converting, etc.).
  • Constant calculations are not available.
  • Potentially uncontrolled growth of memory consumption and the need to artificially limit it.

Because most practical problems requiring the use of fixed-point numbers do not require so much limit on the number of digits, such as usize, but as a rule it is limited:

Unit Precision Decimal digits
United States Dollar (USD) 0.01 2
United States Dollar, stock (USD) 0.0001 4
Bitcoin (BTC) 10-8 8
Ethereum (ETH) 10-18 18

Then most real numbers for financial and other systems requiring accuracy can use 256-bit or even 128-bit integer to store decimal digits.

So In this library, a different approach was chosen.

Big Integers

For big integers this crate provides integer types of arbitrary fixed size which behave exactly like Rust's internal primitive integer types (u8, i8, u16, i16, etc.):

Unit Bits Representation Signed Min Max Helper Macro
I128 128 2 x u64 + -2127 2127-1 int128!(1)
U128 128 2 x u64 0 2128 uint128!(1)
I256 256 4 x u64 + -2255 2255-1 int256!(1)
U256 256 4 x u64 0 2256 uint256!(1)
I512 512 8 x u64 + -2511 2511-1 int512!(1)
U512 512 8 x u64 0 2512 uint512!(1)
I1024 1024 16 x u64 + -21023 21023-1 int1024!(1)
U1024 1024 16 x u64 0 21024 uint1024!(1)
I2048 2048 32 x u64 + -22047 22047-1 int2048!(1)
U2048 2048 32 x u64 0 22048 uint2048!(1)
I4096 4096 64 x u64 + -24095 24095-1 int4096!(1)
U4096 4096 64 x u64 0 24096 uint4096!(1)
I8192 8192 128 x u64 + -28191 28191-1 int8192!(1)
U8192 8192 128 x u64 0 28192 uint8192!(1)

Nearly all methods defined on Rust's signed and unsigned primitive integers are defined fastnum's signed and unsigned integers.

Under the hood bnum is currently used as the backend as most meeting the requirements. Subsequently, the implementation can be replaced in favor of its own implementation, which enables SIMD.

Unsigned integers are stored as an array of digits (primitive unsigned integers) of length N. This means all fastnum integers can be stored on the stack, as they are fixed size. Signed integers are simply stored as an unsigned integer in two's complement.

Decimals

fastnum provides a several decimal numbers suitable for financial calculations that require significant integral and fractional digits with no round-off errors.

Decimal type Integer part Bits Memory representation Signed Max significant_digits Helper macro
D128 U128 128 2 x u64 + i64 + i64 + 2128 dec128!(0.1)
UD128 U128 128 2 x u64 + i64 2128 udec128!(0.1)
D256 U256 256 4 x u64 + i64 + i64 + 2256 dec256!(0.1)
UD256 U256 256 4 x u64 + i64 2256 udec256!(0.1)
D512 U512 512 8 x u64 + i64 + i64 + 2512 dec512!(0.1)
UD512 U512 512 8 x u64 + i64 2512 udec512!(0.1)

Under the hood any [D|UD]N decimal type consists of a N-bit big unsigned integer, paired with a 64-bit signed integer scaling factor which determines the position of the decimal point and sign (for signed types only). Therefore, the precision is not actually arbitrary, but limited to 263 decimal places. Because of this representation, trailing zeros are preserved and may be exposed when in string form. These can be truncated using the normalize or round_dp functions.

Thus, fixed-point numbers are trivially copyable and do not require any dynamic allocation. This allows you to get additional performance gains by eliminating not only dynamic allocation, like such, but also will get rid of one indirect addressing, which improves cache-friendliness and reduces the CPU load.

Why fastnum?

  • Blazing fast: fastnum numerics as fast as native types, well almost :).
  • Trivially copyable types: all fastnum numerics are trivially copyable (both integer and decimal, ether signed and unsigned) and can be stored on the stack, as they are fixed size.
  • No dynamic allocation: no expensive sys-call's, no indirect addressing, cache-friendly.
  • Compile-time integer and decimal parsing: all the from_* methods on fastnum integers and decimals are const, which allows parsing of integers and numerics from string slices and floats at compile time. Additionally, the string to be parsed does not have to be a literal: it could, for example, be obtained via include_str!, or env!.
  • Const-evaluated in compile time macro-helpers: any type has its own macro helper which can be used for definitions of constants or variables whose value is known in advance. This allows you to perform all the necessary checks at the compile time.
  • Small dependencies by default: fastnum does not depend on any other crates by default. Support for crates such as rand and serde can be enabled with crate features.
  • no-std compatible: fastnum can be used in no_std environments.
  • const evaluation: nearly all methods defined on fastnum integers and decimals are const, which allows complex compile-time calculations and checks.

Installation

To install and use fastnum, simply add the following line to your Cargo.toml file in the [dependencies] section:

fastnum = "0.0.3"

Or, to enable various fastnum features as well, add for example this line instead:

fastnum = { version = "0.0.3", features = ["serde"] } # enables the "serde" feature

Example Usage

use fastnum::{udec256, UD256};

fn main() {
    const ZERO: UD256 = udec256!(0);
    const ONE: UD256 = udec256!(1.0);

    let a = udec256!(12345);

    println!("a = {a}");
}

Features

Serialization and Deserialization

The serde feature enables serialization and deserialization of fastnum decimals via the serde crate. More details about serialization and deserialization you can found in

Database ORM's support

The diesel feature enables serialization and deserialization of fastnum decimals for diesel crate.

The sqlx feature enables serialization and deserialization of fastnum decimals for sqlx crate.

Autodocs crates support

The utoipa feature enables support of fastnum decimals for autogenerated OpenAPI documentation via the utoipa crate.

Performance

fastnum is blazing fast. As much as possible given the overhead of arbitrary precision support. It x10 faster than bigdecimal and x1.1 - x4 slower than native floating point f64 Rust type.

Testing

This crate is tested with the rstest crate as well as with specific edge cases.

Minimum Supported Rust Version

The current Minimum Supported Rust Version (MSRV) is 1.82.0.

Documentation

If a method is not documented explicitly, it will have a link to the equivalent method defined on primitive Rust integers (since the methods have the same functionality).

**NB: fastnum is currently pre-1.0.0. As per the Semantic Versioning guidelines, the public API may contain breaking changes while it is in this stage. However, as the API is designed to be as similar as possible to the API of Rust's primitive integers, it is unlikely that there will be a large number of breaking changes. **

Compile-Time Configuration

You can set a few default parameters at compile-time via environment variables:

Environment Variable Default
RUST_FASTNUM_DEFAULT_PRECISION 100
RUST_FASTNUM_DEFAULT_ROUNDING_MODE HalfEven
RUST_FASTNUM_FMT_EXPONENTIAL_LOWER_THRESHOLD 5
RUST_FASTNUM_FMT_EXPONENTIAL_UPPER_THRESHOLD 15
RUST_FASTNUM_FMT_MAX_INTEGER_PADDING 1000
RUST_FASTNUM_DEFAULT_SERDE_DESERIALIZE_MODE Strict

Examine build.rs for how those are converted to constants in the code (if interested).

Future Work

There are several areas for further work:

  • Micro-optimization of big integer types using vector extensions (SSE2, SSE4.2, AVX2, AVX512F, etc.).
  • Const trait implementations once they are stabilized in Rust. (https://github.com/rust-lang/rust/issues/67792)
  • Integration with a large number of crates (ORM's, auto-docs crates, etc.).

Licensing

This code is dual-licensed under the permissive MIT & Apache 2.0 licenses.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Commit count: 6

cargo fmt