Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Performance

Every figure on this page is from one run, committed under crates/ddust/bench/results/: each pass’s transcript and figures, the code size of each operation, and a manifest.toml that says how they were taken. The run was taken on an AWS Graviton4 (Arm Neoverse V2, a c8g.8xlarge) under Linux 6.12.53, built by rustc 1.101.0-nightly of 2026-09-27 in the bench profile (release’s settings with fat LTO, one codegen unit, panic = "abort" and no overflow checks), pinned with taskset to a core the kernel keeps other work off; the manifest records the host’s load before and after.

RunWhat it measures
2026-10-14T06-08Z-pr18-baseline-graviton4every operation of ddust beside every contender, time and counters

The name is the time the run was taken, in UTC, the pull request whose code it measured, what it measured, and the machine.

How It Is Measured

Every crate is built into one binary and measured in one run, since code layout alone moves a figure between builds by a few percent. Each operation is each crate’s fastest correct call, with the rounding mode the crate uses, and ddust is measured again in that mode where a crate’s differs from half to even. The benchmarks’ equivalence test checks every result against an exact reference; a row whose results differ is marked inexact, and is timed all the same.

Each operation runs over two sets of values:

  • Predictable: 1,024 values of one sign and one count of digits, which stay in the level 1 cache: the best case for a branch on the value.
  • Unpredictable: 65,536 values of random signs and counts of digits, more than a branch predictor learns, so a branch on the value mispredicts as it would in a program.

Narrow values are 64-bit at 8 decimals, a price or a quantity as most venues send one; wide values are 128-bit at 18 decimals, a token’s amount as a chain holds it. A crate with one type, as rust_decimal has, is measured at both. Beside the decimals stand the floors, i64, i128 and f64 doing the plain operation a decimal’s is built on: the cost no decimal goes below, which the reference does not check.

Each table lists the crates fastest first on the unpredictable set, with the nanoseconds per operation on each set, and the cycles, instructions and branch misses per operation on the unpredictable set. An operation is timed by throughput, independent operations over the values; the sum, the rounded product and the rounded quotients are timed by latency too, in a chain where each waits for the one before.

Adding and Comparing

A decimal of a static scale is its integer, so adding two is the integer’s add with its overflow check, and comparing two is the integer’s compare:

The checked sum, narrow, 64 bits at 8 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
f640.270.391.083.500.00
fixdec 0.1.0 D640.680.732.037.000.00
primitive_fixed_point_decimal 1.5.0 <i64, 8>0.680.732.037.000.00
nexus-decimal 1.2.2 <i64, 8>0.680.732.047.000.00
fixnum 0.9.5 <i64, U8>0.680.752.097.000.00
fin_decimal 0.4.0 Decimal<8>0.680.752.107.000.00
i640.680.752.117.000.00
ddust D64<8>0.690.762.139.000.00
fixed 1.31.0 I64F640.951.022.8614.00.00
decimal-rs 0.2.06.647.6121.354.70.76
rust_decimal 1.43.07.889.7927.467.51.23
fastnum 0.7.5 D647.7516.646.41261.48
bigdecimal 0.4.1128.638.01063171.78

The checked sum, wide, 128 bits at 18 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
f640.280.391.093.500.00
fixnum 0.9.5 <i128, U18>0.941.032.8814.00.00
i1280.951.042.9114.00.00
primitive_fixed_point_decimal 1.5.0 <i128, 18>0.941.052.9314.00.00
nexus-decimal 1.2.2 <i128, 18>0.941.052.9414.00.00
fin_decimal 0.4.0 Decimal128<18>0.941.052.9314.00.00
ddust D128<18>0.951.062.9414.00.00
decimal-rs 0.2.06.687.6521.454.80.77
rust_decimal 1.43.08.0610.629.767.11.52
fastnum 0.7.5 D1288.8618.250.91411.57
bigdecimal 0.4.1150.547.81344512.27

The order, narrow, 64 bits at 8 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
nexus-decimal 1.2.2 <i64, 8>0.150.210.581.690.00
ddust D64<8>0.150.210.591.690.00
fixdec 0.1.0 D640.150.210.591.690.00
fixnum 0.9.5 <i64, U8>0.150.210.591.690.00
fin_decimal 0.4.0 Decimal<8>0.150.210.591.690.00
i640.150.210.591.690.00
primitive_fixed_point_decimal 1.5.0 <i64, 8>0.150.210.591.690.00
f640.150.210.591.690.00
fixed 1.31.0 I64F640.550.601.697.630.00
decimal-rs 0.2.01.814.1711.725.00.49
bigdecimal 0.4.113.615.6215.743.90.49
rust_decimal 1.43.03.756.5818.445.50.83
fastnum 0.7.5 D648.307.6421.443.01.07

The order, wide, 128 bits at 18 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
f640.150.210.591.690.00
fin_decimal 0.4.0 Decimal128<18>0.530.561.587.630.00
fixnum 0.9.5 <i128, U18>0.540.581.627.630.00
nexus-decimal 1.2.2 <i128, 18>0.550.581.627.630.00
primitive_fixed_point_decimal 1.5.0 <i128, 18>0.530.581.637.630.00
ddust D128<18>0.540.601.687.630.00
i1280.530.611.697.630.00
decimal-rs 0.2.01.834.2111.825.00.49
bigdecimal 0.4.113.636.8319.140.80.80
rust_decimal 1.43.03.757.8421.944.71.02
fastnum 0.7.5 D12815.310.028.056.41.34

The fixed-point crates are within a few percent of each other and of the integer here, since each is the integer underneath; ddust’s checked add takes 9.00 instructions where the i64’s takes 7.00. The two more are the cost of Decimal’s repr(C) layout, which keeps a decimal its integer’s bytes for zerocopy and bytemuck: Rust builds an Option of such a struct in memory, where it returns an Option of a bare integer in two registers. The crates that carry a scale in each value, rust_decimal, fastnum, decimal-rs and bigdecimal, line two scales up before they add, at the cost of branches the unpredictable set mispredicts.

Multiplying

An exact product of a price at 2 decimals and a quantity at 5 is a notional at 7: the integer’s multiply with its overflow check, and nothing for the scale:

The exact product, narrow, 64 bits at 8 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
i640.770.872.4210.00.00
primitive_fixed_point_decimal 1.5.0 <i64, 2> × <i64, 5>0.770.872.4310.00.00
ddust D64<2> × D64<5>0.800.892.5011.00.00
fastnum 0.7.5 D647.177.2020.11220.00
rust_decimal 1.43.07.277.2920.460.00.00
bigdecimal 0.4.1112.320.456.91961.12
decimal-rs 0.2.033.833.894.62310.00

A product rounded back to the factors’ scale is the exact product in an integer twice as wide, divided by or through a multiply by its reciprocal, and the remainder read against half the divisor for the mode; no division instruction is left in it, which just bench-assembly checks on aarch64 and x86_64:

The rounded product, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven0.270.411.133.500.00
i64Trunc0.770.862.4210.00.00
fixed 1.31.0 I64F64Floor2.652.637.3638.00.00
ddust D64<8>Trunc2.873.319.2535.50.15
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand4.023.6810.336.40.18
ddust D64<8>HalfExpand3.293.7310.442.90.15
ddust D64<8>HalfEven3.963.9711.146.90.15
fixdec 0.1.0 D64, inexactTrunc1.694.4912.525.50.49
nexus-decimal 1.2.2 <i64, 8>Trunc3.045.5715.641.50.50
fin_decimal 0.4.0 Decimal<8>HalfEven4.859.6326.964.30.56
fixnum 0.9.5 <i64, U8>HalfExpand14.313.337.31010.71
rust_decimal 1.43.0HalfEven27.830.785.82700.92
fastnum 0.7.5 D64HalfExpand69.933.894.62531.23
decimal-rs 0.2.0HalfExpand41.240.91143140.32
bigdecimal 0.4.11HalfEven1631243481,3294.00

The rounded product, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven0.270.411.143.750.00
i128Trunc3.463.7010.427.10.30
ddust D128<18>Trunc8.836.6218.577.90.08
ddust D128<18>HalfExpand10.17.2220.283.60.09
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand11.77.3020.481.80.07
ddust D128<18>HalfEven10.18.1222.792.00.09
nexus-decimal 1.2.2 <i128, 18>Trunc10.18.6324.173.80.54
fin_decimal 0.4.0 Decimal128<18>HalfEven11.212.936.098.60.79
fixnum 0.9.5 <i128, U18>HalfExpand20.017.448.81411.27
fastnum 0.7.5 D128HalfExpand13244.71253471.68
decimal-rs 0.2.0HalfExpand13052.51473720.81
rust_decimal 1.43.0HalfEven69.757.81623892.03
bigdecimal 0.4.11HalfEven2461714781,7714.82

Division

A rounded quotient is in an integer twice as wide. A narrow numerator that fits in 64 bits is divided by one udiv. On aarch64, a wider one is taken from a double’s estimate, which the core’s pipelined fdiv makes in a few cycles, then corrected to the exact quotient by a comparison, or a step and a comparison, wherever a proof bounds the estimate’s error; a wide quotient, 256 bits by 128, is taken the same way:

The rounded quotient, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven0.890.932.603.750.00
i64Trunc2.172.135.9412.00.00
ddust D64<8>Trunc3.824.4912.646.70.22
ddust D64<8>HalfExpand4.545.3114.952.20.23
ddust D64<8>HalfEven4.705.4015.152.20.24
fixdec 0.1.0 D64Trunc8.008.1222.772.80.45
nexus-decimal 1.2.2 <i64, 8>Trunc8.128.1822.972.80.46
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand11.710.930.571.41.34
fin_decimal 0.4.0 Decimal<8>HalfEven13.611.431.866.40.73
fixnum 0.9.5 <i64, U8>HalfExpand9.9914.340.01000.88
fastnum 0.7.5 D64HalfExpand68.786.22415252.67
rust_decimal 1.43.0HalfEven1041143187632.98
decimal-rs 0.2.0HalfExpand2091995551,1542.68
bigdecimal 0.4.11HalfEven4,2784,41612,34849,10029.78

The rounded quotient, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven0.890.932.593.750.00
i128Trunc6.976.2917.669.30.41
ddust D128<18>Trunc12.713.537.91290.49
ddust D128<18>HalfExpand13.614.440.11380.50
ddust D128<18>HalfEven15.015.844.21400.50
nexus-decimal 1.2.2 <i128, 18>Trunc36.322.262.21721.58
fin_decimal 0.4.0 Decimal128<18>HalfEven35.227.476.62011.47
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand49.629.983.62401.80
fixnum 0.9.5 <i128, U18>HalfExpand39.334.797.02562.41
rust_decimal 1.43.0HalfEven84.998.82766374.33
decimal-rs 0.2.0HalfExpand2341985541,1273.07
fastnum 0.7.5 D128HalfExpand2062065761,3246.70
bigdecimal 0.4.11HalfEven14,83410,12728,315138,45049.13

The same quotients, each dividend by one divisor, as a book is divided by one price:

The rounded quotient by one divisor, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven0.890.922.583.750.00
i64Trunc2.172.406.7212.00.00
ddust D64<8>Trunc2.714.2912.030.30.29
ddust D64<8>HalfExpand3.384.8813.736.70.32
ddust D64<8>HalfEven4.725.9716.752.90.30
fixdec 0.1.0 D64Trunc7.816.8119.169.90.20
nexus-decimal 1.2.2 <i64, 8>Trunc7.806.8119.169.90.20
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand11.28.8124.667.90.77
fin_decimal 0.4.0 Decimal<8>HalfEven13.211.231.366.60.64
fixnum 0.9.5 <i64, U8>HalfExpand10.514.039.197.80.67
fastnum 0.7.5 D64HalfExpand70.480.12245012.48
rust_decimal 1.43.0HalfEven1061093067162.46
decimal-rs 0.2.0HalfExpand2102085821,2482.61
bigdecimal 0.4.11HalfEven4,2514,41112,33547,38123.30

The rounded quotient by one divisor, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven0.890.932.603.500.00
i128Trunc6.857.5221.071.90.65
ddust D128<18>Trunc12.813.638.01290.50
ddust D128<18>HalfExpand13.614.540.51380.50
ddust D128<18>HalfEven15.016.044.71400.50
nexus-decimal 1.2.2 <i128, 18>Trunc38.022.362.41741.39
fin_decimal 0.4.0 Decimal128<18>HalfEven32.823.264.81540.93
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand49.730.084.12431.37
fixnum 0.9.5 <i128, U18>HalfExpand44.234.997.62611.77
rust_decimal 1.43.0HalfEven86.386.92436103.05
fastnum 0.7.5 D128HalfExpand1981714791,1514.51
decimal-rs 0.2.0HalfExpand2272085811,1832.66
bigdecimal 0.4.11HalfEven14,9174,37612,23650,87637.99

A Divisor prepared from that one divisor divides by two multiplications and a comparison, with no division at all; no other crate has one:

The rounded quotient by a prepared divisor, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D64<8>Trunc2.072.075.7829.00.00
ddust D64<8>HalfExpand2.702.707.5535.00.00
ddust D64<8>HalfEven3.013.008.3737.00.00

The rounded quotient by a prepared divisor, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D128<18>Trunc7.889.3226.191.00.48
ddust D128<18>HalfExpand10.411.532.21110.48
ddust D128<18>HalfEven11.112.434.71200.49

A narrow quotient by a prepared divisor takes 3.00 ns where by the decimal it takes 5.97, and a wide one 12.4 where by the decimal it takes 16.0. Preparing it costs a division, so it pays from the second quotient on.

Rescaling

A value rounded from 8 or 18 decimals to 2, a price to cents, divides by a power of ten the compiler knows, through a multiply by its reciprocal:

To cents, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
i64Trunc0.720.722.026.750.00
ddust D64<8>Trunc0.840.842.3610.00.00
f64HalfExpand0.920.892.494.000.00
ddust D64<8>HalfExpand1.141.143.1914.00.00
ddust D64<8>HalfEven1.301.303.6316.00.00
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand1.133.218.9817.00.48
fixdec 0.1.0 D64HalfEven1.193.8210.718.70.34
nexus-decimal 1.2.2 <i64, 8>HalfEven1.884.3212.123.80.33
fin_decimal 0.4.0 Decimal<8>HalfEven1.864.9913.927.00.35
decimal-rs 0.2.0HalfExpand5.326.5718.461.10.32
fastnum 0.7.5 D64HalfExpand12.918.351.31351.08
rust_decimal 1.43.0HalfEven20.221.861.01900.36
bigdecimal 0.4.11HalfEven87.875.52119172.06

To cents, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfExpand0.920.822.304.000.00
ddust D128<18>HalfExpand3.893.9110.931.10.25
ddust D128<18>Trunc3.894.0211.231.10.26
ddust D128<18>HalfEven4.124.1511.633.10.25
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand12.48.3923.558.90.98
i128Trunc10.18.6624.268.50.95
fin_decimal 0.4.0 Decimal128<18>HalfEven12.48.8924.951.30.52
decimal-rs 0.2.0HalfExpand10.99.5826.666.80.71
nexus-decimal 1.2.2 <i128, 18>HalfEven18.514.740.81141.28
fastnum 0.7.5 D128HalfExpand22.024.067.21841.05
rust_decimal 1.43.0HalfEven42.943.71223540.23
bigdecimal 0.4.11HalfEven12690.52531,0102.35

Text

Reading takes a number of up to 32 bytes into 32 vector lanes at once, puts its digits in their steps’ places by one lookup, and makes the number by multiplies in the lanes, with no branch on its length, so the unpredictable set costs nearly what the predictable one does. Each crate reads the text exactly, and refuses a digit past its scale; fin_decimal rounds one instead, and is marked so:

Reading, narrow, 64 bits at 8 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D64<8>9.569.7527.31010.17
rust_decimal 1.43.017.519.153.51751.74
nexus-decimal 1.2.2 <i64, 8>11.619.253.81731.66
f6414.219.554.61691.78
primitive_fixed_point_decimal 1.5.0 <i64, 8>18.820.557.42021.69
fin_decimal 0.4.0 Decimal<8>, inexact15.820.757.82161.68
fixdec 0.1.0 D6421.423.565.72411.51
fastnum 0.7.5 D6420.424.267.62671.68
fixnum 0.9.5 <i64, U8>26.729.482.33601.55
decimal-rs 0.2.037.843.61223842.96
bigdecimal 0.4.1192.891.12551,3731.42

Reading, wide, 128 bits at 18 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D128<18>9.3210.028.195.50.50
nexus-decimal 1.2.2 <i128, 18>16.920.858.32301.38
rust_decimal 1.43.039.830.083.93651.56
f6448.431.889.13291.33
fastnum 0.7.5 D12830.831.989.34291.45
fin_decimal 0.4.0 Decimal128<18>, inexact35.535.298.44251.54
primitive_fixed_point_decimal 1.5.0 <i128, 18>44.039.61104021.58
decimal-rs 0.2.059.762.11745772.13
fixnum 0.9.5 <i128, U18>68.165.91847621.78
bigdecimal 0.4.111291173261,7511.34

Writing splits the steps into their 32 digits by multiplies, and makes each window of the text by one lookup into them; Display, through a formatter, is measured where a crate has nothing faster:

Writing, narrow, 64 bits at 8 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D64<8>9.849.8427.599.00.00
nexus-decimal 1.2.2 <i64, 8>18.423.365.11982.25
i6414.726.574.22152.25
fin_decimal 0.4.0 Decimal<8>17.426.674.42751.83
fixnum 0.9.5 <i64, U8>21.228.579.82681.62
primitive_fixed_point_decimal 1.5.0 <i64, 8>23.933.493.63332.54
fixdec 0.1.0 D6434.240.41134082.12
decimal-rs 0.2.034.444.01235172.20
rust_decimal 1.43.077.767.01876411.45
f6463.867.71896502.98
fastnum 0.7.5 D6464.980.02248892.22
bigdecimal 0.4.111081113121,3911.89

Writing, wide, 128 bits at 18 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D128<18>18.016.445.91770.32
i12820.931.588.22632.62
fixnum 0.9.5 <i128, U18>44.143.01204291.74
fin_decimal 0.4.0 Decimal128<18>58.346.21296111.34
decimal-rs 0.2.037.948.41355512.62
nexus-decimal 1.2.2 <i128, 18>68.449.31384392.29
f6465.874.42087063.50
primitive_fixed_point_decimal 1.5.0 <i128, 18>96.377.62175692.22
rust_decimal 1.43.011891.72569561.48
fastnum 0.7.5 D12883.11032891,1482.66
bigdecimal 0.4.111481393891,6462.40

Floats

A decimal becomes the nearest double. Steps below at up to 22 decimals take one conversion and one division by an exact power of ten, Clinger’s fast path; past them, a division of the steps by a power of five makes the double’s 53 bits and the bits that round them. The predictable set’s steps have 17 and 28 digits, past , so at narrow width it is the slower one here. The crates marked inexact compute the double in floating point, rounding more than once:

To a double, narrow, 64 bits at 8 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
i640.120.150.432.000.00
primitive_fixed_point_decimal 1.5.0 <i64, 8>, inexact0.890.892.502.500.00
fin_decimal 0.4.0 Decimal<8>, inexact0.890.892.502.500.00
fixnum 0.9.5 <i64, U8>, inexact1.231.233.4411.50.00
nexus-decimal 1.2.2 <i64, 8>, inexact1.231.233.4411.50.00
ddust D64<8>4.791.383.8715.60.04
fixdec 0.1.0 D64, inexact1.701.614.5219.00.00
fastnum 0.7.5 D646.212.878.0247.70.07
decimal-rs 0.2.041.110.629.71410.20
rust_decimal 1.43.0, inexact21.723.966.82460.59
bigdecimal 0.4.111091083011,2341.56

To a double, wide, 128 bits at 18 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
i1283.553.559.9443.50.00
primitive_fixed_point_decimal 1.5.0 <i128, 18>, inexact3.613.6010.144.30.00
fin_decimal 0.4.0 Decimal128<18>, inexact3.603.6010.144.30.00
ddust D128<18>7.427.7321.660.70.43
fastnum 0.7.5 D12832.315.543.51360.44
nexus-decimal 1.2.2 <i128, 18>, inexact24.717.047.51530.71
fixnum 0.9.5 <i128, U18>, inexact24.817.047.51530.71
rust_decimal 1.43.0, inexact33.827.376.42740.78
decimal-rs 0.2.081.242.71194961.46
bigdecimal 0.4.111841444011,6011.98

A double becomes the decimal nearest its exact binary value, by the mode. The product of the double and the power of ten, and its residual, which a fused multiply-add computes exactly, decide the rounding below ; past it, the double’s bits are scaled exactly. The crates marked inexact multiply in floating point, or round the double’s shortest text:

From a double, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
i64Trunc0.250.300.843.000.00
fin_decimal 0.4.0 Decimal<8>, inexactTrunc0.430.471.324.750.00
primitive_fixed_point_decimal 1.5.0 <i64, 8>, inexactHalfExpand0.620.621.745.750.00
nexus-decimal 1.2.2 <i64, 8>, inexactHalfExpand1.021.032.8815.00.00
fixdec 0.1.0 D64, inexactHalfExpand1.021.032.8815.00.00
ddust D64<8>HalfEven5.143.9110.952.10.09
fixnum 0.9.5 <i64, U8>, inexactHalfExpand35.792.22585556.28
fastnum 0.7.5 D64HalfExpand63.21764921,1933.62
decimal-rs 0.2.0, inexactHalfExpand1132316451,3047.46
rust_decimal 1.43.0, inexactHalfEven1192607271,27420.49
bigdecimal 0.4.11HalfEven2173439563,8545.08

From a double, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
primitive_fixed_point_decimal 1.5.0 <i128, 18>, inexactHalfExpand2.872.878.0242.00.00
fin_decimal 0.4.0 Decimal128<18>, inexactTrunc3.073.088.6049.00.00
nexus-decimal 1.2.2 <i128, 18>, inexactHalfExpand3.423.449.6153.00.00
i128Trunc2.784.2912.030.20.36
ddust D128<18>HalfEven4.946.3117.766.00.49
fixnum 0.9.5 <i128, U18>, inexactHalfExpand36.813838774312.37
fastnum 0.7.5 D128HalfExpand1041795021,3484.17
decimal-rs 0.2.0, inexactHalfExpand95.72747661,45913.83
rust_decimal 1.43.0, inexactHalfEven1082998351,48526.92
bigdecimal 0.4.11HalfEven2024071,1364,4356.12

Latency

A chain makes each operation wait for the one before, by taking its operands at an index the previous result decides: what a calculation that feeds on its own results costs, where throughput is what a loop over independent values costs. The index takes a byte that depends on both halves of each result, so a compiler that inlines the operation into the chain still computes all of it.

The checked sum, in a chain, narrow, 64 bits at 8 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D64<8>3.573.6710.314.00.00
i643.573.6710.314.00.00
nexus-decimal 1.2.2 <i64, 8>3.563.6810.314.00.00
fixnum 0.9.5 <i64, U8>3.563.6810.314.00.00
fin_decimal 0.4.0 Decimal<8>3.563.6810.314.00.00
primitive_fixed_point_decimal 1.5.0 <i64, 8>3.573.6810.314.00.00
fixdec 0.1.0 D643.563.6910.314.00.00
f644.945.0014.014.00.00
fixed 1.31.0 I64F645.125.2014.619.00.00
decimal-rs 0.2.07.4811.632.565.80.78
rust_decimal 1.43.013.017.849.784.51.20
fastnum 0.7.5 D6420.229.783.21361.43
bigdecimal 0.4.1137.645.61283301.75

The checked sum, in a chain, wide, 128 bits at 18 decimals:

Contenderns, predictablens, unpredictableCyclesInstructionsBranch misses
f644.945.0014.014.00.00
i1285.125.2014.519.00.00
primitive_fixed_point_decimal 1.5.0 <i128, 18>5.125.2014.519.00.00
fixnum 0.9.5 <i128, U18>5.125.2014.519.00.00
ddust D128<18>5.125.2014.519.00.00
nexus-decimal 1.2.2 <i128, 18>5.125.2014.519.00.00
fin_decimal 0.4.0 Decimal128<18>5.125.2014.519.00.00
decimal-rs 0.2.07.4911.632.465.80.77
rust_decimal 1.43.015.720.557.383.11.50
fastnum 0.7.5 D12818.426.774.61551.57
bigdecimal 0.4.1156.655.51554622.22

The rounded product, in a chain, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
i64Trunc4.624.6513.016.00.00
f64HalfEven5.295.3615.014.00.00
fixdec 0.1.0 D64, inexactTrunc5.207.5321.126.00.47
ddust D64<8>Trunc10.28.0822.631.90.14
fixed 1.31.0 I64F64Floor8.678.6624.241.00.00
ddust D64<8>HalfExpand10.69.0025.234.80.14
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand10.99.1025.434.20.17
ddust D64<8>HalfEven12.49.4626.539.30.15
nexus-decimal 1.2.2 <i64, 8>Trunc10.612.936.033.50.45
fin_decimal 0.4.0 Decimal<8>HalfEven11.714.239.755.00.58
fixnum 0.9.5 <i64, U8>HalfExpand22.116.345.796.50.77
rust_decimal 1.43.0HalfEven36.739.11092860.87
fastnum 0.7.5 D64HalfExpand84.643.91232631.24
decimal-rs 0.2.0HalfExpand52.745.01262950.33
bigdecimal 0.4.11HalfEven1621243471,3284.02

The rounded product, in a chain, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven5.295.3615.014.00.00
i128Trunc3.767.7321.629.10.30
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand21.914.139.572.90.06
ddust D128<18>Trunc17.915.342.986.90.10
nexus-decimal 1.2.2 <i128, 18>Trunc20.115.743.966.70.51
ddust D128<18>HalfExpand19.115.744.192.60.10
ddust D128<18>HalfEven19.917.749.51010.09
fin_decimal 0.4.0 Decimal128<18>HalfEven23.119.554.71090.81
fixnum 0.9.5 <i128, U18>HalfExpand30.823.565.71511.19
fastnum 0.7.5 D128HalfExpand15053.71503611.70
decimal-rs 0.2.0HalfExpand14857.71613540.83
rust_decimal 1.43.0HalfEven85.464.51804051.99
bigdecimal 0.4.11HalfEven2461714781,7704.84

The rounded quotient, in a chain, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
i64Trunc4.974.9513.915.00.00
f64HalfEven8.618.7324.514.00.00
ddust D64<8>Trunc14.610.529.433.50.22
ddust D64<8>HalfExpand15.312.234.040.00.22
ddust D64<8>HalfEven16.112.434.739.90.24
fixdec 0.1.0 D64Trunc17.516.044.874.80.47
nexus-decimal 1.2.2 <i64, 8>Trunc17.516.044.874.80.47
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand21.816.145.063.81.30
fin_decimal 0.4.0 Decimal<8>HalfEven20.816.245.259.70.78
fixnum 0.9.5 <i64, U8>HalfExpand16.918.250.993.90.91
fastnum 0.7.5 D64HalfExpand82.795.62675352.68
rust_decimal 1.43.0HalfEven1091183297642.98
decimal-rs 0.2.0HalfExpand2202075801,1412.67
bigdecimal 0.4.11HalfEven4,2694,41012,33249,17930.47

The rounded quotient, in a chain, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven8.618.7324.514.00.00
i128Trunc14.510.128.277.30.36
ddust D128<18>Trunc30.330.685.51430.49
nexus-decimal 1.2.2 <i128, 18>Trunc50.631.187.01821.59
ddust D128<18>HalfExpand31.431.788.61520.50
ddust D128<18>HalfEven33.133.292.81540.50
fin_decimal 0.4.0 Decimal128<18>HalfEven46.137.21042161.50
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand60.839.71112551.82
fixnum 0.9.5 <i128, U18>HalfExpand43.940.91152712.45
rust_decimal 1.43.0HalfEven93.31052926384.31
decimal-rs 0.2.0HalfExpand2432085801,1153.08
fastnum 0.7.5 D128HalfExpand2172186091,3386.62
bigdecimal 0.4.11HalfEven14,8299,95827,849138,62748.67

The rounded quotient by one divisor, in a chain, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
i64Trunc4.975.2414.715.00.00
f64HalfEven8.628.7424.514.00.00
ddust D64<8>Trunc14.511.532.134.40.29
ddust D64<8>HalfExpand15.313.036.340.70.26
ddust D64<8>HalfEven16.213.337.340.60.28
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand21.415.844.360.50.78
nexus-decimal 1.2.2 <i64, 8>Trunc17.116.044.971.90.20
fixdec 0.1.0 D64Trunc17.116.044.971.90.20
fin_decimal 0.4.0 Decimal<8>HalfEven20.516.646.559.80.70
fixnum 0.9.5 <i64, U8>HalfExpand17.018.351.291.30.67
fastnum 0.7.5 D64HalfExpand83.390.52535112.48
rust_decimal 1.43.0HalfEven1091133167172.46
decimal-rs 0.2.0HalfExpand2222206161,2362.61
bigdecimal 0.4.11HalfEven4,2514,39212,28547,47622.86

The rounded quotient by one divisor, in a chain, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
f64HalfEven8.618.8024.614.00.00
i128Trunc14.511.632.478.90.60
ddust D128<18>Trunc30.330.685.51430.49
nexus-decimal 1.2.2 <i128, 18>Trunc51.231.487.91841.37
ddust D128<18>HalfExpand31.331.688.41520.50
ddust D128<18>HalfEven33.133.292.71540.49
fin_decimal 0.4.0 Decimal128<18>HalfEven42.833.493.51690.95
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand60.840.81142581.36
fixnum 0.9.5 <i128, U18>HalfExpand47.941.91172761.79
rust_decimal 1.43.0HalfEven92.593.12606223.01
fastnum 0.7.5 D128HalfExpand2111835121,1654.44
decimal-rs 0.2.0HalfExpand2402196141,1792.68
bigdecimal 0.4.11HalfEven14,9074,35212,17150,97037.22

The rounded quotient by a prepared divisor, in a chain, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D64<8>Trunc6.136.1317.234.00.00
ddust D64<8>HalfExpand7.597.6021.340.00.00
ddust D64<8>HalfEven8.638.6424.243.00.00

The rounded quotient by a prepared divisor, in a chain, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableCyclesInstructionsBranch misses
ddust D128<18>Trunc13.814.440.294.00.49
ddust D128<18>HalfExpand17.217.749.51140.49
ddust D128<18>HalfEven19.419.755.11230.50

A Mix, and Code Size

A program mixes operations, and what each brings into the instruction cache is then a cost of its own. The mix runs a sum, an order, a rounded product, a rounded quotient and a value to cents in turn over the same values, with the level 1 instruction cache’s misses and the front end’s stalled cycles per operation:

The mix, narrow, 64 bits at 8 decimals:

ContenderModens, predictablens, unpredictableL1i missesFront-end stalls
i64Trunc0.480.470.0000.00
f64HalfEven, HalfExpand0.490.480.0000.00
ddust D64<8>Trunc1.181.410.0000.40
ddust D64<8>HalfExpand1.401.610.0000.36
ddust D64<8>HalfEven1.661.730.0000.37
fixdec 0.1.0 D64, inexactTrunc, HalfEven2.293.210.0001.30
nexus-decimal 1.2.2 <i64, 8>Trunc, HalfEven2.573.380.0001.21
primitive_fixed_point_decimal 1.5.0 <i64, 8>HalfExpand3.533.600.0001.88
fin_decimal 0.4.0 Decimal<8>HalfEven3.284.850.0001.31
fastnum 0.7.5 D64HalfExpand35.531.10.0017.33
rust_decimal 1.43.0HalfEven33.335.10.0016.29
decimal-rs 0.2.0HalfExpand51.851.10.0014.03
bigdecimal 0.4.11HalfEven9279310.05881.5

The mix, wide, 128 bits at 18 decimals:

ContenderModens, predictablens, unpredictableL1i missesFront-end stalls
f64HalfEven, HalfExpand0.490.480.0000.00
i128Trunc3.543.490.0001.05
ddust D128<18>Trunc5.214.750.0001.17
ddust D128<18>HalfExpand5.965.270.0001.33
ddust D128<18>HalfEven6.385.680.0001.24
primitive_fixed_point_decimal 1.5.0 <i128, 18>HalfExpand13.88.400.0002.78
nexus-decimal 1.2.2 <i128, 18>Trunc, HalfEven11.78.520.0002.90
fin_decimal 0.4.0 Decimal128<18>HalfEven10.99.770.0002.48
rust_decimal 1.43.0HalfEven46.642.20.0019.23
decimal-rs 0.2.0HalfExpand74.253.30.0014.99
fastnum 0.7.5 D128HalfExpand79.960.00.00313.3
bigdecimal 0.4.11HalfEven3,0962,0740.452130

Each operation’s code, in instructions, with every function it calls but the panics and ddust’s cold paths, which a value past 19 decimals or a result past its integer reaches; read from the release build’s disassembly on aarch64:

Operationddust D64<8>ddust D128<18>nexus-decimalprimitive_fixed_point_decimalfin_decimalfixnumrust_decimalfastnumdecimal-rsbigdecimal
add101255559553,1841,6099,888
compare56555514232210310,189
mul-round10321569641012815873,2951,29511,060
div-round3736742483301642801,2674,6031,85412,338
div-round-prepared48132
rescale-round23914328332213,21426210,359
parse1,1861,1893616163795981,0515355111,356
format3431,7243331291,248129129129129132
to-f64701301977193761771,99310,163
from-f64141101181511488467130,34553010,916

A call through a pointer, which core::fmt makes, is not followed: a crate that writes text through Display counts only the code around the formatter. ddust’s reader and writer count their general paths, for a number past 32 bytes or 30 decimals, beside their vector ones.

What to Keep off a Hot Path

  • A wide division where a narrow one does. A D128 quotient costs about three times a D64’s, and a chain of them two to three times; a quantity, a price or a cash amount fits a D64.
  • Dividing many values by one decimal. A Divisor prepared once divides with no division.
  • A conversion to a double inside a calculation. It is correctly rounded and cheap, but the double is rarely the decimal, and a calculation that goes through one loses the exactness ddust keeps.

How It Was Checked

The benchmarks’ equivalence test checks every result against an exact reference, on both sets and at both widths. The kernels that divide only by constants, the add, the compare, the rounded product, the rescale, the reader, the writer and both float conversions, are held by just bench-assembly to contain no division instruction and call no division routine, on aarch64 and x86_64. Each figure is the median of the five passes’ medians: 124 of the run’s 782 times spread past 2% across the passes, half of them operations under two nanoseconds, where 2% is under a twentieth of one, and the run’s summary.toml keeps each one’s range. It runs with:

just bench-run 18 baseline graviton4 --passes 5