How IEEE 754 stores floating point numbers in binary — the anatomy of float32 and float64, denormalized numbers, NaN, infinity, and why 0.1 + 0.2 does not equal 0.3.
IEEE 754 is the technical standard for floating point arithmetic used by virtually every modern CPU, GPU, and programming language. It defines how real numbers are represented in binary, how arithmetic operations behave, and how exceptional conditions (division by zero, overflow, invalid operations) are handled. I have worked with it across C, Java, JavaScript, Python, and Rust — the bit layout is always the same.
A floating point number in IEEE 754 consists of three fields packed into a fixed-width binary word:
When I was optimizing a physics simulation, I used the binary representation of floats to implement a fast inverse square root. Reinterpreting the float bits as an integer, subtracting from a magic constant, and shifting gave a surprisingly good approximation.
The value is computed as: (-1)^sign * (1 + mantissa) * 2^(exponent - bias)
The leading "1" before the decimal point is implicit — it is not stored. This is called the implicit leading bit trick, and it gives you one extra bit of precision for free. The bias ensures that the stored exponent is always positive, making comparisons easier to implement in hardware.
A 32-bit float (also called binary32 or single precision) uses 1 bit for the sign, 8 bits for the exponent, and 23 bits for the mantissa. The exponent bias is 127. This format can represent values from approximately 1.4e-45 to 3.4e38, with about 7 decimal digits of precision.
Float32 bit layout: 31 | 30:23 | 22:0 Sign|Exponent|Mantissa 0 |01111100|01000000000000000000000 Bit positions: [31] Sign [30-23] Exponent (8 bits, bias = 127) [22-0] Mantissa (23 bits, implicit leading 1) # Example: converting 5.75 to IEEE 754 float32 # Step 1: Convert to binary 5 = 101 (binary) 0.75 = 0.11 (binary: 0.5 + 0.25) 5.75 = 101.11 # Step 2: Normalize to 1.xxxx * 2^n 101.11 = 1.0111 * 2^2 # Step 3: Extract fields Sign = 0 (positive) Exponent = 2 + 127 = 129 = 10000001 Mantissa = 01110000000000000000000 (fractional part only, drop the leading 1) # Result: 0 10000001 01110000000000000000000 # Hex: 0x40B80000
Let me decode a float from its hex representation to verify the math:
// Decode 0x40B80000 back to decimal
int bits = 0x40B80000;
int sign = (bits >> 31) & 1; // 0
int exponent = (bits >> 23) & 0xFF; // 129
int mantissa = bits & 0x7FFFFF; // 0x380000
// Reconstruct the value
int realExponent = exponent - 127; // 2
double value = Math.pow(-1, sign)
* (1 + mantissa / Math.pow(2, 23))
* Math.pow(2, realExponent);
// value = 5.75 ✓
Minimum positive normal: 1.1754944e-38. Maximum finite: 3.4028235e38. Machine epsilon (difference between 1 and the next representable float): about 1.19e-7. If your computation requires more than 7 significant digits, switch to double. Use our 32-bit programmer calculator to explore the binary representation of any float.
A 64-bit double (binary64 or double precision) uses 1 bit for the sign, 11 bits for the exponent, and 52 bits for the mantissa, with a bias of 1023. This gives about 15-17 decimal digits of precision and a range from 5e-324 to 1.8e308.
Float64 bit layout: 63 | 62:52 | 51:0 Sign|Exponent|Mantissa 0 |10000000001|1000000000000000000000000000000000000000000000000000 # Example: converting 12.25 to IEEE 754 float64 12.25 in binary = 1100.01 Normalized: 1.10001 * 2^3 Sign = 0 (positive) Exponent = 3 + 1023 = 1026 = 10000000010 Mantissa = 1000100000000000000000000000000000000000000000000000 # Result: 0 10000000010 1000100000000000000000000000000000000000000000000000 # In JavaScript (which uses float64 for all numbers): const view = new DataView(new ArrayBuffer(8)); view.setFloat64(0, 12.25); const high = view.getUint32(0); // 0x40288000 const low = view.getUint32(4); // 0x00000000
I learned the hard way that JavaScript's Number is always IEEE 754 float64. There is no integer type — every number is a double. This means integers up to 2^53 (about 9 quadrillion) are represented exactly, but beyond that, precision loss occurs. If you are doing bit operations on large integers in JS, our 64-bit programmer calculator uses BigInt to avoid this trap.
The IEEE 754 standard reserves certain exponent values to represent special cases. Understanding these is critical for writing robust numerical code — I have debugged production issues where a NaN silently propagated through a calculation and corrupted downstream results.
| Exponent | Mantissa | Meaning | Example (float32) |
|---|---|---|---|
| 0x00 (all zeros) | 0 | Zero (+0 and -0) | 0x00000000 / 0x80000000 |
| 0x00 (all zeros) | Non-zero | Denormalized (subnormal) | 0x00400000 = smallest positive denormal |
| 0xFF (all ones) | 0 | Infinity (+inf and -inf) | 0x7F800000 / 0xFF800000 |
| 0xFF (all ones) | Non-zero | NaN (quiet or signaling) | 0x7FC00000 = quiet NaN |
| 0x01 to 0xFE | Any | Normal number | 0x40B80000 = 5.75 |
Denormalized numbers (or subnormals) fill the underflow gap between zero and the smallest normal number. Instead of the implicit leading 1, denormals have a leading 0, sacrificing precision to represent very small values. They are much slower on most CPUs because they require microcode assistance — if you care about performance and know you will never need subnormals, many CPUs let you flush them to zero.
// Checking for special float values (C)
#include <math.h>
float x = some_value;
if (isnan(x)) { /* not a number */ }
if (isinf(x)) { /* positive or negative infinity */ }
if (isnormal(x)) { /* normal (not zero, subnormal, inf, or nan) */ }
// In Java:
float x = some_value;
if (Float.isNaN(x)) { /* NaN */ }
if (Float.isInfinite(x)) { /* infinity */ }
if (Float.isFinite(x)) { /* finite (not NaN and not infinity) */ }
// NaN is never equal to itself — this is by design in IEEE 754
float nan = Float.NaN;
System.out.println(nan == nan); // false!
System.out.println(nan != nan); // true!
The fact that NaN is never equal to itself is a deliberate IEEE 754 design choice. It enables consistent behavior across expressions like 0.0 / 0.0, sqrt(-1.0), and infinity - infinity. In Java, the Float.NaN != Float.NaN expression evaluates to true, which is unusual for a comparison but mathematically correct for NaN propagation.
The classic example that confuses every programmer at some point: 0.1 + 0.2 != 0.3. This is not a bug in any language — it is a fundamental property of binary floating point representation.
# In Python (IEEE 754 double precision):
0.1 + 0.2 # 0.30000000000000004
0.1 + 0.2 == 0.3 # False
# In JavaScript (also IEEE 754 double precision):
0.1 + 0.2 // 0.30000000000000004
# In C:
float a = 0.1f;
float b = 0.2f;
printf("%.10f\n", a + b); // 0.3000000119 (different rounding in float vs double!)
The reason: in binary, 0.1 is a repeating fraction, just like 1/3 is in decimal. The binary representation of 0.1 is 0.00011001100110011... repeating forever. Since IEEE 754 can only store a finite mantissa (23 bits for float, 52 for double), the value is rounded. Adding two rounded values gives a result that is slightly off from the mathematically exact answer.
Here are the practical rules I follow to avoid floating point surprises in production code:
// Rule 1: Never compare floats for equality directly
// Wrong
if (a + b == 0.3) { ... }
// Right — use an epsilon comparison
const double EPSILON = 1e-9;
if (fabs((a + b) - 0.3) < EPSILON) { ... }
// Rule 2: Use integer arithmetic for money
// Wrong
double price = 19.99;
double tax = price * 0.08; // 1.5992... might round wrong
// Right
int cents = 1999;
int taxCents = cents * 8 / 100; // integer arithmetic, exact
// Rule 3: Prefer double over float unless memory is critical
// float has ~7 digits of precision; double has ~15-17
// In Java and JavaScript, float is less common — stick with double
Our 32-bit programmer calculator lets you inspect the exact IEEE 754 bit pattern of any single-precision float. Use it to see how common decimal values like 0.1, 0.5 (exact in binary!), and 3.14159 look inside a computer's floating point unit.
Our 32-bit and 64-bit programmer calculators show the full binary representation of any number, including the IEEE 754 bit layout. Enter a decimal value and see how it breaks down into sign, exponent, and mantissa.
IEEE 754 stores a floating point number in three parts: sign (1 bit), exponent (8 bits for float, 11 for double), and mantissa/significand (23 bits for float, 52 for double). The value is computed as (-1)^sign * (1 + mantissa) * 2^(exponent - bias). The bias is 127 for float32 and 1023 for float64. This format allows representing values from about 1.4e-45 to 3.4e38 for single precision.
First, convert the integer and fractional parts to binary separately. Then normalize the binary number so it has exactly one digit before the decimal point. The exponent is the number of places you shifted, plus the bias. The mantissa is the fractional part after the leading 1. For example, 5.75 in binary is 101.11; normalized to 1.0111 * 2^2; exponent = 2 + 127 = 129 (10000001); mantissa = 01110000000000000000000.
Special exponent values encode exceptional cases. An exponent of all 1s with a zero mantissa represents infinity (positive or negative based on sign bit). An exponent of all 1s with a non-zero mantissa represents NaN (Not a Number). An exponent of all 0s with a non-zero mantissa represents denormalized numbers — subnormal values close to zero that use a leading 0 instead of 1, gradually losing precision.
Binary cannot represent 0.1 or 0.2 exactly, just as decimal cannot represent 1/3 exactly. In binary, 0.1 is the repeating fraction 0.0001100110011... When stored as a float or double, it is rounded to fit the mantissa. When you add two rounded values, the result is slightly off. 0.1 + 0.2 in IEEE 754 double precision gives 0.30000000000000004. This is fundamental to binary floating point — not a bug.
Float (single precision) uses 32 bits: 1 sign, 8 exponent, 23 mantissa — about 7 decimal digits of precision. Double (double precision) uses 64 bits: 1 sign, 11 exponent, 52 mantissa — about 15-17 decimal digits. Double is the default in JavaScript, Python, and most Java computations. Use float only when memory or bandwidth is constrained, such as GPU vertex buffers or large arrays where 50% memory savings matter.