Scientific Computing · Floating Point
IEEE 754 Binary Formats: Sign, Exponent, and Fraction Layouts
How binary16, binary32, binary64, and binary128 divide a fixed-width word into three fields, why the exponent is biased, and what the implicit leading bit buys.
IEEE 754 does not define a single floating-point format. It defines a family parameterised by two integers, and the four binary interchange formats in common use are four points in that family. Everything else the standard specifies — rounding rules, exception flags, the behaviour of infinities — sits on top of the layout described here. Getting the layout exactly right makes the rest of the subject predictable, because most floating-point surprises are really statements about which values are representable at all.
A format is a precision and an exponent range
Two parameters define a binary format: the precision p, which counts significant bits including the leading one, and the exponent range [emin, emax]. The four interchange formats fix them as follows.
| Format | Total bits | Exponent bits w | Trailing bits t | Precision p | Bias | emin | emax |
|---|---|---|---|---|---|---|---|
| binary16 | 16 | 5 | 10 | 11 | 15 | −14 | 15 |
| binary32 | 32 | 8 | 23 | 24 | 127 | −126 | 127 |
| binary64 | 64 | 11 | 52 | 53 | 1023 | −1022 | 1023 |
| binary128 | 128 | 15 | 112 | 113 | 16383 | −16382 | 16383 |
The word is split into three fields: one sign bit s, w exponent bits holding an unsigned integer E, and t = p − 1 trailing significand bits holding an unsigned integer f. Nothing else is stored. There is no separate field for the leading bit of the significand, and no field for the radix point, because both are implied by the encoding rules.
The widths are a storage decision, and the four formats show the trade directly. Moving from binary32 to binary64 doubles the word and buys 29 more significand bits and eight more exponent bits, which is roughly nine more decimal digits of precision and a largest-finite-value ratio of about 2^896. binary16 keeps only 10 significand bits, about three decimal digits, and its largest finite value is 65504, so it is usable only where the data is known to be small and precision is secondary. binary128 exists mainly for interchange of values produced by extended-precision arithmetic, not because hardware commonly computes in it.
The bias is always 2^(w−1) − 1. For binary32 that is 127; for binary64 it is 1023. The choice is not arbitrary, and it interacts with the next rule.
For a stored exponent E with 1 ≤ E ≤ 2^w − 2, the encoded value is
$$ (-1)^s \times 2^{E - \text{bias}} \times \left(1 + \frac{f}{2^t}\right) $$
The factor 1 + f/2^t lies in [1, 2). That is the whole normal-number case: a value is a sign, a power of two drawn from a bounded range, and a fraction of a binade carried to t bits.
Why the exponent is biased instead of signed
A two’s-complement exponent field would be the obvious encoding, and it would break a property the standard wants: for two non-negative finite values, comparing their bit patterns as unsigned integers must give the same answer as comparing the values. With sign-magnitude plus a bias, the encoding is monotone. Larger exponent field means larger power of two; for equal exponents, larger fraction field means larger value.
That monotonicity is what makes integer comparison usable for sorting arrays of floats, for min/max reductions, and for the total-ordering predicate the standard defines for the special values. It fails for negative numbers, where the sign bit dominates: a negative float with a large magnitude has a large unsigned pattern but a small value. The usual fix when a monotone key is needed across the whole range is to flip every bit when the sign bit is set, or to compare sign first and magnitude second.
The bias also gives a natural place to put the two exceptional exponent-field values. E = 0 and E = 2^w − 1 are reserved, and both are needed because the hidden bit cannot be represented in the all-zeros case.
The hidden bit, and what it costs
For a normal value the leading significand bit is always 1, since the significand is scaled into [1, 2). Storing it would waste one bit per value. By convention the bit is implicit, so binary32 carries 24 bits of precision in a 32-bit word: 1 sign, 8 exponent, 23 stored fraction. The same accounting gives binary64 its 53 bits from 64.
The price of the convention is that the exponent field’s zero value can no longer mean “exponent emin with a leading 1”. It is redefined as “no leading 1”, which is exactly what a subnormal is: significand f/2^t in [0, 1) scaled by 2^emin. Subnormals therefore fill the gap between zero and the smallest normal number, at reduced precision, and the all-zeros fraction with E = 0 encodes zero itself.
The gap is worth quantifying. For binary32 the smallest positive normal is 2^-126, about 1.17549435e-38, and the smallest positive subnormal is 2^-149, about 1.4e-45. The subnormal range spans 23 binades of decreasing precision, ending in a single-bit value. Without subnormals the distance from zero to the smallest normal would be a cliff, and the difference of two nearby small numbers would flush to zero. The trade-off — and the reason some hardware offers a flush-to-zero mode — is discussed in Subnormals, Signed Zero, and the Cost of Gradual Underflow.
Decoding a word
The fields are extracted by shifting and masking. In C the safe way to reinterpret the bits of a float is memcpy into an unsigned integer of the same width; casting a pointer and dereferencing it violates the strict-aliasing rules.
#include <stdint.h>
#include <string.h>
struct decoded {
int sign;
int exponent; /* raw field, 0..255 */
uint32_t fraction; /* 23 bits */
};
static struct decoded decode_binary32(float x)
{
uint32_t bits;
memcpy(&bits, &x, sizeof bits); /* defined; pointer casts are not */
struct decoded d = {
.sign = (int)(bits >> 31),
.exponent = (int)((bits >> 23) & 0xFFu),
.fraction = bits & 0x7FFFFFu,
};
return d;
}
The decoded triple maps to a value by four cases, and the case analysis is the format definition:
E = 0,f = 0: the value is±0, depending on the sign bit.E = 0,f ≠ 0: a subnormal,(-1)^s × 2^-126 × (f / 2^23).1 ≤ E ≤ 254: a normal,(-1)^s × 2^(E−127) × (1 + f / 2^23).E = 255,f = 0:±∞.E = 255,f ≠ 0: a NaN.
Two constants fall out of the encoding. The largest finite binary32 is (2 − 2^-23) × 2^127, about 3.4028235e38; the largest finite binary64 is (2 − 2^-52) × 2^1023, about 1.7976931348623157e308. The spacing between adjacent values in [1, 2) is 2^-23 for binary32 and 2^-52 for binary64, which is the sense of “machine epsilon” used by most C programmers. That spacing is not the same quantity as the unit roundoff 2^-p, the bound on relative error from one rounding; the distinction matters in error analysis and is taken up in Round-to-Nearest-Even and the Mechanics of Correct Rounding.
The all-ones exponent field
Setting every exponent bit to 1 removes the value from the finite range entirely. With a zero fraction the result is infinity, which exists so that overflow has a defined answer and so that 1/0 and -1/0 differ in sign rather than trapping. With a nonzero fraction the result is NaN, and the fraction field is free to carry a payload.
The standard distinguishes two NaN kinds by the most significant fraction bit: clear means signalling, set means quiet. For binary32 that leaves 22 payload bits; for binary64, 51. A signalling NaN raises the invalid-operation exception when consumed by most operations, which is the mechanism behind the traditional trick of initialising memory to a signalling NaN pattern to catch reads of uninitialised data. In practice the distinction has decayed: many language runtimes and compilers canonicalise NaN results, and the standard does not require payload propagation through arithmetic. Counting encodings makes the space concrete — binary32 has 2 × (2^23 − 1) = 16,777,214 distinct NaN bit patterns, and a program has no portable way to predict which one an operation will produce.
What the layout does not pin down
The interchange formats are mandatory for data exchange, but a C implementation is not obliged to make float binary32. FLT_RADIX, FLT_MANT_DIG, and FLT_MAX_EXP in <float.h> describe the actual type, and the standard permits radices other than two. Extended formats with wider exponent ranges are also allowed, which is the historical origin of the x87 80-bit type with its explicit integer bit and 15-bit exponent.
Three further freedoms in the encoding matter for numerical work. The sign of a NaN produced by an operation is unspecified. The rounding mode is a property of the dynamic environment, not of the format, so the same bit pattern decodes to the same value but arithmetic on it does not. And the standard defines the set of representable values, not the order in which an implementation evaluates an expression — an expression tree may be reassociated unless the language forbids it, a subject that determines whether two machines compute identical bits from identical source.
References
- IEEE 754-2019: Standard for Floating-Point Arithmetic — IEEE
- What Every Computer Scientist Should Know About Floating-Point Arithmetic — David Goldberg, ACM Computing Surveys 23(1), 1991
- Handbook of Floating-Point Arithmetic, 2nd edition — Muller, Brunie, de Dinechin, Jeannerod, Joldes, Lefevre, Melquiond, Revol, Torres; Birkhauser