> Source: https://upvera.tech/guides/deterministic-numbers/
> The same calculation can give different numbers because floating-point arithmetic rounds after every operation, so the order of a sum, the compiler, the processor and the math library can all change the last digits. To make it deterministic, compute in integers at a fixed scale with a written rounding rule, fix the order of every reduction, and avoid platform math functions. Then serialize results canonically, hash them, and check the hashes against golden test vectors on every target you ship.

[Guides](/guides/)Explainers

Explainer · · For developers · Value Engine

# Why the same calculation gives different numbers, and how to make it deterministic

Short answer

The same calculation can give different numbers because floating-point arithmetic rounds after every operation, so the order of a sum, the compiler, the processor and the math library can all change the last digits. To make it deterministic, compute in integers at a fixed scale with a written rounding rule, fix the order of every reduction, and avoid platform math functions. Then serialize results canonically, hash them, and check the hashes against golden test vectors on every target you ship.

Computed in your browser just now

| Expression | Floating point gives | Integer fixed point gives |
| --- | --- | --- |
| `0.1 + 0.2`Neither 0.1 nor 0.2 has an exact binary form. | … | 100000 + 200000 = 300000 → 0.300000 |
| `(1e16 + 1) - 1e16`The 1 is lost when it is added to a number this large. | … | integers: (10¹⁶ + 1) − 10¹⁶ = 1 |
| `1e16 - 1e16 + 1`Same numbers, different order, different answer. | … | integers: 10¹⁶ − 10¹⁶ + 1 = 1 |
| `[0.1, 0.2, 0.3].reduce(+)`Summing left to right. | … | 100000 + 200000 + 300000 = 600000 |
| `[0.3, 0.2, 0.1].reduce(+)`Summing right to left gives a different float. | … | 300000 + 200000 + 100000 = 600000 |

## What the demo above shows

Most software stores numbers in IEEE 754 binary floating point. Neither 0.1 nor 0.2 has an exact binary form, so each is stored as the nearest value the format can hold, and their sum is rounded again. That is why `0.1 + 0.2` prints `0.30000000000000004`.

Near 10^16, neighbouring double-precision numbers are 2 apart. In `(1e16 + 1) - 1e16`, the 1 is rounded away when it meets the large number, and the result is `0`. In `1e16 - 1e16 + 1`, the large numbers cancel first and the result is `1`. Same numbers, different order, different answer: floating-point addition is not associative. The last two rows show the same on a list: `0.6000000000000001` from the left, `0.6` from the right.

A single operation is not the problem. IEEE 754 requires basic arithmetic to be exactly rounded, so one operation on the same inputs and precision repeats bit for bit. The drift comes from everything around it.

## Where the differences come from

-   **Order of operations.** A sum over a hash map, a database query without `ORDER BY` or a parallel reduction can add the same values in a different order each run.
-   **Compiler choices.** A compiler may turn `a * b + c` into one fused multiply-add that rounds once, not twice. GCC does so by default outside strict C standards modes (`-ffp-contract`), and `-ffast-math` also lets it reorder operations.
-   **Extra precision.** The x87 unit, GCC's default on most 32-bit x86 targets, keeps intermediates in 80 bits. A value is rounded to 64 bits when the compiler stores it to memory, so results can depend on register use.
-   **Math library functions.** ECMAScript lets `Math.sin`, `Math.exp` and similar functions give an implementation-approximated result, so engines may disagree in the last bit. Rust documents the precision of `f64::sin` as varying by platform and version.
-   **WebAssembly corners.** WebAssembly follows IEEE 754, but NaN signs and payloads are non-deterministic, and relaxed SIMD instructions can depend on the hardware.
-   **Rounding and division rules.** Python's `round(2.5)` gives `2`; JavaScript's `Math.round(2.5)` gives `3`. Rust truncates `-7 / 2` to `-3`, while Python's `-7 // 2` floors to `-4`.

## How to make a calculation deterministic

### 1\. Use integers at a fixed scale

Pick a unit and store every quantity as a whole number of it. In millionths, 0.1 is `100000` and addition is exact. Multiplication and division need a wider intermediate and one written rounding rule:

```rust
const SCALE: i128 = 1_000_000; // whole millionths

/// a × b at the same scale, rounding half away from zero.
fn mul_e6(a: i64, b: i64) -> i64 {
    let p = a as i128 * b as i128;
    let q = if p >= 0 { (p + SCALE / 2) / SCALE } else { (p - SCALE / 2) / SCALE };
    i64::try_from(q).expect("overflow")
}
```

The rule matters less than writing it down and using it everywhere. Integer addition is associative, so the order of a sum stops mattering as long as no partial sum can overflow. Use checked operations: Rust panics on overflow in debug builds but may wrap silently in release builds.

### 2\. Fix the order

Sort inputs by a stable key before any reduction that is not exact, and add `ORDER BY` to every query that feeds a calculation. For floats you cannot avoid, the latest ECMAScript adds `Math.sumPrecise`, which sums exactly and rounds once; not every engine ships it yet.

### 3\. Own your math functions

Do not call the platform's `sin`, `exp`, `log` or `pow` where results must repeat. Ship your own integer approximations or tables, so every target runs the same steps.

### 4\. Serialize canonically and hash

Two results are equal only if their bytes are. Serialize with a canonical form such as the JSON Canonicalization Scheme (RFC 8785): sorted keys, fixed number formatting, no extra whitespace. It limits numbers to what a double can hold, so larger integers go in strings. Hash the bytes and compare digests.

### 5\. Test golden vectors on every target

Keep inputs and their expected output digests in the repository, and run them on every build for every target you ship: native, WebAssembly, each operating system and processor. A mismatch fails the build.

### 6\. Make the build reproducible

Pin the compiler and every dependency, so anyone with the same source, environment and instructions can recreate bit-for-bit identical artifacts. A hash of the build then names the exact code behind a number.

## Common mistakes

-   **Rounding for display, then reusing the result.** A value shown with two decimals is no longer the value you computed.
-   **Parsing amounts with `JSON.parse`.** Every JSON number becomes a double. Integers above `Number.MAX_SAFE_INTEGER` (9007199254740991) lose precision; send them as strings and use `BigInt`.
-   **Rounding in different places.** Rounding each row on one screen and only the total on another gives two answers.
-   **Reading the clock or a random source inside the calculation.** Pass time in as an input instead.

## Questions

### Should I use a decimal type instead?

Decimal types such as Python's `decimal` hold 0.1 exactly but still round on division, so every target needs the same precision settings. Fixed-scale integers travel more easily between languages.

### Do WebAssembly and native code give the same results?

Integer arithmetic matches when both sides use the same widths and overflow rules. Floats usually match too, except for NaN bits, relaxed SIMD and library functions. Golden vectors tell you for certain.

### How many golden vectors do I need?

Enough to cover each rounding branch, both signs, zero, the largest allowed values and every overflow path. Choose them for edges, not volume.

## How Upvera does it

Upvera Value Engine 3.0 computes Upvera Value in integer fixed point, and a lint that runs on every build rejects floating-point arithmetic in the engine. The engine is written in Rust. Its WebAssembly build, which Upvera's servers run, and a native build of the same code must produce byte-identical outputs on a shared set of golden test vectors. Outputs are serialized as canonical JSON and hashed, so equality is a comparison of digests. The engine reads no clock and uses no randomness. Upvera Value is computed by Upvera and has no unit; it is not a price and not what a company is worth.

Receipts list each reason as an integer in whole millionths, so the rows add up exactly to the change they explain. The design is written up in the [Value Engine report](/value/).

## Sources

-   [What every computer scientist should know about floating-point arithmetic](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)
-   [ECMAScript: Math.sin](https://tc39.es/ecma262/#sec-math.sin) and [Math.sumPrecise](https://tc39.es/ecma262/#sec-math.sumprecise)
-   [Rust f64](https://doc.rust-lang.org/std/primitive.f64.html), [integer division](https://doc.rust-lang.org/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators) and [integer overflow](https://doc.rust-lang.org/reference/behavior-not-considered-unsafe.html#integer-overflow)
-   [GCC optimize options](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html) and [x86 options](https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html)
-   [WebAssembly numerics](https://webassembly.github.io/spec/core/exec/numerics.html)
-   [RFC 8785: JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html)
-   [Reproducible builds: definition](https://reproducible-builds.org/docs/definition/)

## Related

[UTR-26-02**Value Engine**Same events, same rule, same number. With a receipt.System reportRead](/value/)[Tool**Canonicalize JSON (RFC 8785) and hash it**Turn any JSON into its RFC 8785 canonical form and SHA-256 it, in your browser, with a real example that links one day to the next.DevelopersUse it](/guides/json-canonicalization/)[Guide**How to verify a hash chain of daily records**Verify a real chain of daily digests, step by step, with about 20 lines of JavaScript you can run in your browser.Anyone checkingRead](/guides/verify-hash-chain/)
