Photo by Franck V. on Unsplash

NaN Boxing: Encoding Types in Unused Float Bits


Dynamic languages face a fundamental problem: every value needs both data and type information at runtime. A naive approach stores these separately, consuming extra memory and cache space. NaN boxing solves this by exploiting unused bit patterns in IEEE 754 floating-point numbers to encode both type tags and values in a single 64-bit word.

The technique takes advantage of a quirk in the IEEE 754 double-precision format. Not all 64-bit patterns represent valid numbers. When the exponent bits are all ones, the value is either infinity or NaN (not a number). The standard defines millions of distinct NaN patterns, but most programs only need one. This leaves a vast space of unused bit patterns available for other purposes.

How the Encoding Works

A 64-bit double has one sign bit, 11 exponent bits, and 52 mantissa bits. When the exponent is 0x7FF (all ones), the number is special. If the mantissa is zero, it’s infinity. If the mantissa is non-zero, it’s NaN. IEEE 754 distinguishes between signaling and quiet NaNs using the highest mantissa bit, but CPUs typically canonicalize NaNs anyway, making most of these patterns unused in practice.

NaN boxing repurposes these unused NaN patterns. A runtime might reserve one bit pattern range for actual doubles and use the rest to encode:

  • Small integers (common in many programs)
  • Pointers to heap objects (strings, arrays, closures)
  • Special constants (null, undefined, true, false)
  • Type tags that identify what kind of pointer or value follows

A typical scheme uses the top bits as a discriminator. If the value matches the bit pattern of a valid double (including actual NaN values), it’s interpreted as a number. Otherwise, the bottom bits encode either an immediate value or a pointer, with a few bits reserved to distinguish between types.

Memory and Performance Benefits

Without NaN boxing, a dynamic language value might need 16 bytes: 8 for a type tag and 8 for data. With NaN boxing, every value fits in 8 bytes. This halves memory usage and doubles cache efficiency. When processing arrays or passing arguments, fewer cache lines are consumed.

The performance impact extends beyond memory. Modern CPUs predict branch patterns, but type checks in interpreters create unpredictable branches that stall the pipeline. NaN boxing concentrates type information into the same word as the data, improving spatial locality and making type tests cheaper. A single comparison can distinguish numbers from everything else, and subsequent bit masking extracts the specific type or payload.

For JavaScript engines, this matters. V8, JavaScriptCore, and SpiderMonkey all use variants of NaN boxing or pointer tagging. Lua’s reference implementation uses a similar approach. When a program manipulates millions of values per second, the savings compound.

Implementation Constraints

NaN boxing isn’t free. It assumes pointers fit in the available bits. On 64-bit systems, user-space pointers typically use only 48 bits, leaving 16 bits for tags. This works until you need more pointer space or finer-grained type distinctions. Some runtimes run out of tag space and resort to secondary type fields for complex objects, undermining the optimization.

Another constraint involves NaN canonicalization. x86 and ARM handle NaNs differently. Some operations preserve NaN payloads, others canonicalize them to a standard bit pattern. A runtime must either avoid operations that destroy NaN payloads or carefully extract and restore them around such operations. This limits which floating-point instructions can be used directly.

Debugging suffers too. A NaN-boxed value in a debugger looks like a nonsensical float rather than a meaningful pointer or integer. Tooling needs specialized pretty-printers, and memory dumps become harder to interpret without decoding logic.

Alternatives and Evolution

Pointer tagging is a related technique that stores tag bits in the low-order bits of pointers, exploiting alignment guarantees. If objects are aligned to 8-byte boundaries, the bottom three bits are always zero and can encode type information. This approach is simpler but only offers a few tag bits, limiting the number of immediate types.

Some modern runtimes use hybrid schemes. Common small integers might get their own encoding, while less frequent types fall back to heap-allocated tagged pointers. Others use different strategies on 32-bit versus 64-bit platforms, where pointer size and NaN bit availability differ.

As hardware evolves, these techniques adapt. ARM’s top-byte ignore feature and x86-64’s 5-level paging extend usable pointer bits, squeezing tag space. Meanwhile, proposals for tagged memory architectures might provide hardware-level type support, potentially obsoleting software tricks like NaN boxing.

For now, NaN boxing remains a pragmatic optimization, turning an obscure floating-point detail into a way to make dynamic languages faster and leaner.