How Wasm Handles Floating-Point Non-Determinism
WebAssembly (Wasm) is designed to be a highly portable, fast, and deterministic execution environment. However, achieving absolute determinism across different hardware architectures is challenging when dealing with floating-point operations, as modern CPUs handle certain edge cases differently. This article explains how the WebAssembly specification balances execution speed and determinism by allowing limited non-determinism in floating-point “Not-a-Number” (NaN) propagation, and how developers can achieve strict determinism when necessary.
The Root of the Problem: IEEE 754 and NaN Payloads
The IEEE 754 standard for floating-point arithmetic defines how computers handle real numbers. While it specifies the results of basic arithmetic operations, it leaves certain behaviors undefined or implementation-dependent. The primary source of non-determinism in WebAssembly arises from NaN (Not-a-Number) payloads.
A NaN value is represented by a specific bit pattern where the exponent bits are all set to one. The remaining bits, known as the “payload,” can carry diagnostic information. The IEEE 754 standard does not mandate how the sign bit or the payload bits of a NaN are preserved or modified during operations. Consequently, x86 and ARM processors may output different binary representations (bit patterns) for the same NaN-producing operation.
How the WebAssembly Specification Handles NaNs
To ensure that Wasm can run at near-native speeds, the specification avoids mandating a single, strict NaN representation for all operations. Forcing a specific NaN pattern on an architecture that naturally generates a different one would require the runtime to insert expensive masking and branching instructions after every floating-point operation.
Instead, WebAssembly introduces a controlled form of non-determinism. The specification categorizes NaNs into two groups:
- Canonical NaN: A unique, standardized NaN representation with a specific bit pattern (the sign bit is 0, the quiet bit is 1, and all other payload bits are 0).
- Arithmetic NaN: Any floating-point bit pattern that represents a NaN according to IEEE 754, with no restrictions on the sign bit or the payload.
Under the WebAssembly specification, any floating-point operation that results in a NaN is permitted to return either the canonical NaN or any arithmetic NaN. This allows the underlying CPU to use its native instruction set directly, maximizing performance while acknowledging that the exact binary output of a NaN might differ between an x86 server and an ARM-based mobile device.
Affected Operations
This relaxed determinism only applies to operations that produce or propagate NaNs. These include:
- Standard arithmetic operations (e.g.,
f32.add,f64.div) when the inputs or outputs result in a NaN. - Floating-point conversions, promotions, and demotions (e.g.,
converting an
f64NaN to anf32NaN). - The
f32.copysignandf64.copysignoperations under certain edge cases.
Operations that do not involve NaNs, such as normal addition, subtraction, multiplication, and division of real numbers, remain fully deterministic across all compliant hardware architectures.
Achieving Strict Determinism
While the default behavior of WebAssembly allows this minor floating-point divergence, certain use cases—such as blockchain smart contracts, replicated state machines, and multiplayer game physics—demand 100% bit-level determinism.
To achieve strict determinism in WebAssembly, developers and runtime environments use a technique called NaN Canonicalization. This process ensures that any NaN produced during execution is immediately forced into a single, uniform bit pattern.
This can be achieved in two ways:
- Software-level sanitization: The Wasm binary is compiled or post-processed to insert bitwise operations after floating-point instructions. If the result of an operation is a NaN, a bit-mask is applied to convert it to the canonical NaN format.
- Runtime-level instrumentation: Specialized Wasm engines (such as those used in blockchain platforms like Near or Polkadot) automatically instrument the compiled machine code to enforce canonical NaNs, prioritizing execution consistency over raw hardware speed.