Fixed-size decimal numbers implemented in pure Rust. Suitable for financial, crypto and any other fixed-precision calculations.
This crate is inspired by num_bigint and bigdecimal – amazing crates that allow 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 tiny numbers, for example,
0
or1
. - 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.
fastnum
provides signed and unsigned exact precision decimal numbers suitable for financial calculations that
require significant integral and fractional digits with no round-off errors (such as 0.1 + 0.2 ≠ 0.3).
Any fastnum
decimal type consists of an N-bit big unsigned integer, paired with a 64-bit signaling block which
contains a 16-bit scaling factor determines the position of the decimal point, sign, special and signaling flags.
Trailing zeros are preserved and may be exposed when in string form.
Thus, fixed-point numbers are trivially copyable and don't 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.
- Strictly exact precision: no round-off errors (such as 0.1 + 0.2 ≠ 0.3).
- Special values:
fastnum
support±0
,±Infinity
andNaN
special values with [IEEE 754] semantic. - Blazing fast:
fastnum
numerics are 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're 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 onfastnum
integers and decimals areconst
, 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 viainclude_str!
, orenv!
. - 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.
- Short dependencies list by default:
fastnum
depends only uponbnum
by default. All other dependencies are optional. Support for crates such asrand
andserde
can be enabled with crate features. no-std
compatible:fastnum
can be used inno_std
environments.const
evaluation: nearly all methods defined onfastnum
integers and decimals areconst
, which allows complex compile-time calculations and checks.
To install and use fastnum
, simply add the following line to your Cargo.toml
file in the [dependencies]
section:
fastnum = "0.1"
Or, to enable various fastnum
features as well, add for example this line instead:
fastnum = { version = "0.1", features = ["serde"] } # enables the "serde" feature
use fastnum::*;
fn main() {
const ZERO: UD256 = udec256!(0);
const ONE: UD256 = udec256!(1.0);
let a = udec256!(12345);
println!("a = {a}");
}
The numtraits
feature includes implementations of traits from the
num_traits
crate, e.g.
AsPrimitive
,
Signed
, etc.
The rand
feature allows creation of random fastnum
decimals via the rand
crate.
The serde
feature enables serialization and deserialization of fastnum
decimals via the
serde
crate. More details about serialization and deserialization you can found
in
The zeroize
feature enables the Zeroize
trait from
the zeroize
crate.
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.
The utoipa
feature enables support of fastnum
decimals for autogenerated OpenAPI documentation via the
utoipa
crate.
f64 |
fastnum |
bigdecimal |
rust_decimal |
decimal_rs |
|
---|---|---|---|---|---|
Size, bits | 64(52) | 64/128/256/512/... | any | 128(96) | 64 |
Max precision, decimal digits | arbitrary* | arbitrary | 38 | 19 | |
Exact precision | ✅ | ✅ | ✅ | ✅ | |
Trivially copyable / No dynamic allocation | ✅ | ✅ | ✅ | ✅ | |
Exceptional conditions such as inexact , subnormal , round , etc. |
✅ | ||||
±0 , ±Infinity and NaN special values |
✅ | ✅ | |||
Compile-time calculations | ✅ | ✅ | |||
Performance | 🚀🚀🚀🚀🚀 | 🚀🚀🚀🚀 | 🚀 | 🚀🚀🚀🚀 | 🚀🚀🚀🚀 |
no-std |
✅ | ✅ | ✅ |
* Precision is arbitrary but fixed.
fastnum
is blazing fast. As much as possible given the overhead of arbitrary precision support.
Some benchmark reports are shown below:
Decimal digits | f64 |
fastnum |
bigdecimal |
---|---|---|---|
4 | 12.782 ns | 11.810 ns | 86.252 ns |
11 | 12.759 ns | 17.175 ns | 92.105 ns |
20 | 13.426 ns | 27.096 ns | 115.91 ns |
22 | 38.531 ns | 30.746 ns | 127.37 ns |
33 | 38.102 ns | 42.282 ns | 143.59 ns |
39 | 38.295 ns | 48.973 ns | 151.37 ns |
52 | 42.090 ns | 83.334 ns | 178.53 ns |
72 | 42.856 ns | 112.30 ns | 215.94 ns |
Allocate vec![]
with N
elements.
N |
f64 |
fastnum(D128) |
bigdecimal |
---|---|---|---|
100 | 62.099 ns | 59.355 ns | 1.7866 µs |
500 | 63.436 ns | 209.75 ns | 8.7757 µs |
1000 | 102.07 ns | 396.30 ns | 17.402 µs |
10000 | 769.01 ns | 4.2774 µs | 178.33 µs |
100000 | 10.777 µs | 43.982 µs | 1.8032 ms |
1000000 | 108.94 µs | 448.66 µs | 18.395 ms |
Perform a + b
.
Decimal digits | f64 |
fastnum |
bigdecimal |
---|---|---|---|
2 | 582.68 ps | 28.809 ns | 86.501 ns |
24 | 536.08 ps | 10.890 ns | 51.743 ns |
39 | 574.84 ps | 20.899 ns | 89.070 ns |
40 | 562.10 ps | 32.838 ns | 137.64 ns |
100 | 562.10 ps | 83.700 ns | 119.52 ns |
Perform a - b
.
Decimal digits | f64 |
fastnum |
bigdecimal |
---|---|---|---|
2 | 654.78 ps | 21.761 ns | 89.013 ns |
12 | 622.80 ps | 21.815 ns | 89.317 ns |
24 | 526.57 ps | 12.354 ns | 51.006 ns |
39 | 574.51 ps | 20.839 ns | 90.768 ns |
40 | 557.47 ps | 32.524 ns | 132.43 ns |
76 | 534.51 ps | 33.343 ns | 89.778 ns |
154 | 622.19 ps | 83.844 ns | 159.80 ns |
Perform a × b
.
Decimal digits | f64 |
fastnum |
bigdecimal |
---|---|---|---|
3 | 512.51 ps | 8.8683 ns | 57.996 ns |
30 | 529.00 ps | 8.7431 ns | 56.618 ns |
44 | 609.04 ps | 9.0473 ns | 106.84 ns |
88 | 617.74 ps | 74.798 ns | 463.48 ns |
Perform a ÷ b
.
Decimal digits | f64 |
fastnum |
bigdecimal |
---|---|---|---|
1 | 554.17 ps | 12.181 ns | 96.252 ns |
13 | 607.92 ps | 84.582 ns | 175.94 ns |
77 | 592.75 ps | 1.8675 µs | 270.37 µs |
154 | 572.60 ps | 13.297 µs | 269.63 µs |
You can run benchmark tests with Criterion.rs
tool:
cd benchmark
cargo criterion
This crate is tested with the rstest
crate as well as with specific edge
cases.
We have more than 10'000
tests, so we recommend running it using nextest
:
cargo nextest run --all-features
The current Minimum Supported Rust Version (MSRV) is 1.82.0
.
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 types, it is unlikely that there will be a large number of breaking
changes.
You can set a few default parameters at compile-time via environment variables:
Environment Variable | Default |
---|---|
RUST_FASTNUM_DEFAULT_ROUNDING_MODE |
HalfUp |
RUST_FASTNUM_FMT_EXPONENTIAL_LOWER_THRESHOLD |
5 |
RUST_FASTNUM_FMT_EXPONENTIAL_UPPER_THRESHOLD |
15 |
RUST_FASTNUM_FMT_MAX_INTEGER_PADDING |
1000 |
RUST_FASTNUM_SERDE_DESERIALIZE_MODE |
Strict |
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're stabilized in Rust. (rust-lang/rust#67792)
- Integration with a large number of crates (ORM's, auto-docs crates, etc.).
This code is dual-licensed under the permissive MIT & Apache 2.0 licenses.
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.