Skip to main content

Floating Point: IEEE 754

Overviewโ€‹

Floating point is a way to represent a huge range of real numbers โ€” from 1e-300 to 1e300 โ€” in a fixed number of bits, by storing a sign, an exponent, and a fraction (mantissa) instead of every digit. Almost every language's float/double follows IEEE 754, a standard chosen specifically so results are bit-for-bit reproducible across different hardware. The tradeoff: most decimal fractions (like 0.1) have no exact binary representation, which is the root cause of nearly every "floating point is broken" bug report.

Core Conceptsโ€‹

TermMeaning
Sign bit1 bit: 0 for positive, 1 for negative.
ExponentStored as a biased unsigned integer; determines the scale (power of 2).
Mantissa (significand)The significant digits of the number, with an implicit leading 1 for normal values.
BiasA constant subtracted from the stored exponent to get the true exponent (127 for single, 1023 for double).
Subnormal (denormal)A tiny value close to zero, represented without the implicit leading 1, trading precision for range.
NaN"Not a Number" โ€” result of an undefined operation like 0.0 / 0.0.

Architecture / Mechanismโ€‹

IEEE 754 binary32 (float) and binary64 (double) split their bits as follows:

binary32 (32 bits total):
[S][ Exponent (8) ][ Fraction (23) ]
31 30 23 22 0
sign, bias=127

binary64 (64 bits total):
[S][ Exponent (11) ][ Fraction (52) ]
63 62 52 51 0
sign, bias=1023

Worked through with real bits:

A 32-bit float shown bit by bit: sign bit 0, exponent bits 01111100, and a fraction beginning 01 followed by zeros, annotated as equalling 0.15625
The bit pattern for 0.15625. Sign 0 (positive); exponent 01111100 = 124, which is 124 โˆ’ 127 = โˆ’3 after removing the bias; fraction 0.01โ‚‚, giving 1.25 ร— 2โปยณ. Wikimedia Commons, CC BY 3.0

Two details in that picture cause most floating-point surprises. The exponent is biased, not signed โ€” you subtract 127 to get the real one, which is what makes bit patterns compare in the same order as the numbers they encode. And the leading 1. of the mantissa is implicit: it is never stored, which buys an extra bit of precision but means the encoding cannot represent zero at all without a special case (exponent bits all zero), the first of several such carve-outs alongside infinity and NaN.

The value is reconstructed as:

value = (-1)^sign ร— 1.fraction (binary) ร— 2^(exponent - bias)

Special cases use reserved exponent patterns:

Exponent bitsFractionMeaning
All zeroZeroยฑ0
All zeroNon-zeroSubnormal: (-1)^sign ร— 0.fraction ร— 2^(1-bias) (no implicit leading 1)
All oneZeroยฑInfinity
All oneNon-zeroNaN

Why 0.1 + 0.2 != 0.3โ€‹

0.1 in binary is an infinitely repeating fraction (0.0001100110011...), just like 1/3 is 0.333... in decimal. It gets rounded to the nearest representable double, so 0.1 and 0.2 are each already slightly off before any addition happens โ€” and the sum lands on a different representable value than the rounded 0.3.

Practical Usageโ€‹

#include <cstdio>
#include <cmath>

double a = 0.1 + 0.2;
printf("%.17f\n", a); // 0.30000000000000004 โ€” not exactly 0.3
printf("%s\n", (a == 0.3) ? "eq" : "not eq"); // "not eq"

// Correct way to compare floats: use a tolerance (epsilon), not ==
bool nearly_equal = std::fabs(a - 0.3) < 1e-9;

double nan_val = std::nan("");
bool is_nan = std::isnan(nan_val); // true
bool nan_eq_itself = (nan_val == nan_val); // false โ€” NaN is never equal to anything, even itself

Edge Cases & Pitfallsโ€‹

Never use floating point for money

Currency needs exact decimal arithmetic (cents must never silently drift). Floating point rounding error compounds across many additions/multiplications. Use integer cents, a fixed-point type, or a decimal type (std::decimal proposals, language-specific Decimal/BigDecimal types, or database DECIMAL/NUMERIC columns) instead.

NaN breaks normal comparison logic

NaN != NaN is true, so std::sort and set/map ordering can misbehave if NaNs sneak into comparisons โ€” always check std::isnan() before comparing or sorting untrusted floating-point data.

  • == on floating-point results of independent computations is almost always the wrong tool โ€” rounding differences from a different instruction order (even mathematically equivalent code) can change the last bit.
  • Subnormals preserve "gradual underflow" near zero but are often computed much slower in hardware; some codebases explicitly flush them to zero for performance.

Comparisonsโ€‹

FormatTotal bitsExponent bitsFraction bitsApprox. decimal digits
binary32 (float)328 (bias 127)23~7
binary64 (double)6411 (bias 1023)52~15-17
Fixed-point / integer centsn (any)โ€”โ€”Exact, no rounding error

Referencesโ€‹

Books & Videosโ€‹

  • Randal E. Bryant & David R. O'Hallaron, Computer Systems: A Programmer's Perspective โ€” Chapter 2 covers IEEE 754 in detail alongside integer representation.
  • Computerphile, "Floating Point Numbers" โ€” a short, accessible explanation of why 0.1 + 0.2 != 0.3.