Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 2 – Bits, bytes and numbers

Floating-point numbers bit by bit

Decode IEEE 754 float and double into sign, exponent and fraction, see why 0.1 + 0.2 misses 0.3, and move the bits to an integer register on three ISAs.

  • Beginner
  • 15 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Decode an IEEE 754 binary32 and binary64 value into its sign, exponent and fraction
  • Explain NaN, the infinities, subnormals and why 0.1 and 0.2 do not add to 0.3
  • Distinguish a quiet NaN from a signalling one by its fraction bits
  • Move floating-point bits into an integer register with movq, fmov or fmv.x.d
What you’ll learn

What you will learn

  • Decode an IEEE 754 binary32 and binary64 value into its sign, exponent and fraction
  • Explain NaN, the infinities, subnormals and why 0.1 and 0.2 do not add to 0.3
  • Distinguish a quiet NaN from a signalling one by its fraction bits
  • Move floating-point bits into an integer register with movq, fmov or fmv.x.d
On this page

A program stores 0.1 in eight bytes, and those bytes are 0x3FB999999999999A: not a tenth, but the nearest value a double can hold, 0.1000000000000000055. That is why 0.1 + 0.2 prints 0.30000000000000004, and why a floating-point number: A number stored as a sign, an exponent and a fraction, so one format covers a wide range by moving a binary point. binary32 (C float) and binary64 (C double) are the common sizes; the value is roughly 1.fraction times 2 to the exponent. number is really three fields packed into its bits, a sign, an exponent and a fraction, laid out by the IEEE 754: The standard for floating-point arithmetic that x86-64, AArch64 and RISC-V all follow. It fixes the binary16, binary32, binary64 and binary128 formats, how each lays out a sign, exponent and fraction, and how results are rounded. standard that every one of the three ISAs follows.

Below, you decode those fields for a 64-bit double and a 32-bit float, find where NaN, the infinities and the subnormal numbers sit, work the 0.1 + 0.2 rounding out to the last bit, and move a value’s bits into an integer register with one instruction on each ISA — movq on x86-64, fmov on AArch64 and fmv.x.d on RISC-V — so you can read them.

Three fields: sign, exponent and fraction

IEEE 754 binary64, the C double, divides its 64 bits into three fields: 1 sign bit, then 11 exponent bits, then 52 fraction bits. The value is (-1)^sign × 1.fraction × 2^(exponent − 1023). The leading 1. is not stored, because a normal number always has it; the exponent is held with a bias of 1023 added, so the stored field 1025 means an exponent of 2. binary32, the C float, uses the same shape with smaller fields: 1 sign bit, 8 exponent bits with a exponent bias: A constant added to the real exponent so the stored exponent field is a non-negative integer that still covers negative powers. binary64 adds 1023 and binary32 adds 127, so a stored field equal to the bias means an exponent of 0. of 127, and 23 fraction bits.

This program unpacks the bytes with struct and masks out each field. It decodes -6.25, which is exact, and 0.1, which is not, in both formats:

Decoding a double and a float into three fields Python · decode/decode.py
"""Splits a double (binary64) and a float (binary32) into sign, exponent and fraction, then rebuilds each value."""
import struct


def fields(x, fmt, uint, exp_bits, frac_bits):
    """The bit pattern of x in format fmt, and its three fields."""
    bits = struct.unpack(uint, struct.pack(fmt, x))[0]
    sign = bits >> (exp_bits + frac_bits)
    exponent = (bits >> frac_bits) & ((1 << exp_bits) - 1)
    fraction = bits & ((1 << frac_bits) - 1)
    return bits, sign, exponent, fraction


def show(name, x, fmt, uint, exp_bits, frac_bits, bias):
    bits, s, e, f = fields(x, fmt, uint, exp_bits, frac_bits)
    width = (1 + exp_bits + frac_bits) // 4
    binary = f"{bits:0{1 + exp_bits + frac_bits}b}"
    significand = 1 + f / 2**frac_bits
    value = (-1) ** s * significand * 2.0 ** (e - bias)
    print(f"{name} {x}: 0x{bits:0{width}X}")
    print(f"  bits         {binary[0]} {binary[1:1 + exp_bits]} {binary[1 + exp_bits:]}")
    print(f"  sign         {s}")
    print(f"  exponent     field {e} - bias {bias} = {e - bias}")
    print(f"  fraction     0x{f:X}")
    print(f"  significand  1 + 0x{f:X} / 2**{frac_bits} = {significand!r}")
    print(f"  value        {'-' if s else '+'}{significand!r} * 2**{e - bias} = {value!r}")


show("double", -6.25, "<d", "<Q", 11, 52, 1023)
show("double", 0.1, "<d", "<Q", 11, 52, 1023)
show("float", 0.1, "<f", "<I", 8, 23, 127)

Output

double -6.25: 0xC019000000000000
  bits         1 10000000001 1001000000000000000000000000000000000000000000000000
  sign         1
  exponent     field 1025 - bias 1023 = 2
  fraction     0x9000000000000
  significand  1 + 0x9000000000000 / 2**52 = 1.5625
  value        -1.5625 * 2**2 = -6.25
double 0.1: 0x3FB999999999999A
  bits         0 01111111011 1001100110011001100110011001100110011001100110011010
  sign         0
  exponent     field 1019 - bias 1023 = -4
  fraction     0x999999999999A
  significand  1 + 0x999999999999A / 2**52 = 1.6
  value        +1.6 * 2**-4 = 0.1
float 0.1: 0x3DCCCCCD
  bits         0 01111011 10011001100110011001101
  sign         0
  exponent     field 123 - bias 127 = -4
  fraction     0x4CCCCD
  significand  1 + 0x4CCCCD / 2**23 = 1.600000023841858
  value        +1.600000023841858 * 2**-4 = 0.10000000149011612

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 decode.py

For -6.25 the sign is 1, the stored exponent is 1025 (so 2 after the bias), and the significand: The fraction part of a floating-point number, the digits of its value apart from the exponent. For a normal number it is 1.fraction with a leading 1 that is implied and not stored; binary64 keeps 53 bits of it, binary32 keeps 24. 1 + 0x9000000000000 / 2^52 is 1.5625; −1.5625 × 2^2 = −6.25, exact. For 0.1 the double’s fraction is 0x999999999999A, a rounded repeating pattern, so the stored value is 0.1000000000000000055, not a tenth. The float stores even fewer bits, 0x3DCCCCCD, about 0.10000000149. A double keeps 53 bits of significand (the hidden bit plus 52), nearly 16 significant decimal digits; a float keeps 24 bits, about 7.

See any number's bits

Step through the same decoding for -6.25, from its 64 stored bits back to its value:

Interactive · stage by stage Decoding 0xC019000000000000 into -6.25 Step through the three fields of a double and rebuild the value they stand for.
Decoding 0xC019000000000000 into -6.25, step 5 of 5. Showing 0xC019000000000000, sign bit 1, exponent 1025, fraction 0x9…, -1.5625 x 2^2. In focus: -1.5625 x 2^2.0xC019000000000000the 64 stored bitssign bit 1the number is negativeexponent 1025minus bias 1023 = 2fraction 0x9…significand 1.5625-1.5625 x 2^2= -6.25

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The stored bits. The double holds 0xC019000000000000, which is 1100 0000 0001 1001 then 48 zeros in binary. Read it as three fields from the top down.
  2. The sign bit. The top bit is 1, so the number is negative. The sign is always the single highest bit, whatever the exponent and fraction hold.
  3. The exponent field. The next 11 bits are 100 0000 0001, which is 1025. Subtract the bias of 1023 to get the real exponent, 2. The bias lets one unsigned field cover both negative and positive exponents.
  4. The fraction field. The last 52 bits are 0x9000000000000. The significand is 1 plus that fraction over 2^52, which is 1.5625: the leading 1 is implied for a normal number and never stored.
  5. Rebuild the value. Put it together: -1 times 1.5625 times 2^2 is -6.25, the exact value decode.py prints. Every normal double is read this one way.

Key idea

A binary64 value is (-1)^sign × 1.fraction × 2^(exponent − 1023): a sign bit, an 11-bit biased exponent and a 52-bit fraction, with the leading 1 of the significand never stored.

Checkpoint

Check: The three fields of a double

  1. Question 1 of 2 A double's 11-bit exponent field holds 1024. With the binary64 bias of 1023, what is the real exponent?

    Type a number.

    A hint

    The stored field is the real exponent plus the bias; subtract the bias.

    Show the answer: Question 1 of 2

    Answer: 1

    binary64 adds a bias of 1023 to the real exponent before storing it, so the real exponent is 1024 − 1023 = 1. A field of 1024 with a half fraction is the value 1.5 × 2^1 = 3.0.

  2. Question 2 of 2 How many bits does each field of an IEEE 754 binary32 float take, from the top bit down?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 1 sign, 8 exponent, 23 fraction

    A binary32 float is 1 sign bit, then 8 exponent bits (bias 127), then 23 fraction bits. A double keeps the same shape with 11 exponent bits (bias 1023) and 52 fraction bits.

Why 0.1 and 0.2 do not add to 0.3

One tenth is 0.000110011001100… in binary, a fraction that repeats for ever, so no finite field can hold it exactly. The double nearest to 0.1 is a hair above it, and so is the one nearest to 0.2. Add those two rounded values and the exact sum lands between two doubles; the processor must round again, and the nearest double to the sum is one step above the double nearest to 0.3. The result, 0x3FD3333333333334, is 0.30000000000000004, which is why the comparison 0.1 + 0.2 == 0.3 is false.

This program prints the stored value of each literal to its last decimal digit, then the exact sum before the final rounding:

0.1 + 0.2, rounded twice Python · point-three/point_three.py
"""Why 0.1 + 0.2 is not 0.3: each literal is rounded once, and the sum is rounded again."""
import math
import struct
from decimal import Decimal, getcontext

getcontext().prec = 80  # enough digits to add the exact stored values without rounding


def hex64(x):
    return f"0x{struct.unpack('<Q', struct.pack('<d', x))[0]:016X}"


def steps(x):
    """A double between 0.25 and 0.5 as a whole number of steps of 2**-54: its 53-bit significand."""
    return int(Decimal(x) * 2**54)


for text, x in [("0.1", 0.1), ("0.2", 0.2), ("0.3", 0.3)]:
    print(f"{text} is stored as {hex64(x)}")
    print(f"   = {Decimal(x)}")

total = 0.1 + 0.2
print(f"0.1 + 0.2 is stored as {hex64(total)}")
print(f"   = {Decimal(total)}")
print(f"repr {total!r}, equal to 0.3: {total == 0.3}")

exact = Decimal(0.1) + Decimal(0.2)  # the true sum of the two stored values
below, above = 0.3, math.nextafter(0.3, 1.0)
print("exact sum of the stored values, before rounding:")
print(f"   = {exact}")
print(f"halfway between {hex64(below)} and {hex64(above)}: {exact == (Decimal(below) + Decimal(above)) / 2}")
print(f"their significands: {steps(below)} (odd) and {steps(above)} (even)")
print(f"ties to even gives {hex64(total)}, one step ({math.ulp(0.3)}) above the double for 0.3")

Output

0.1 is stored as 0x3FB999999999999A
   = 0.1000000000000000055511151231257827021181583404541015625
0.2 is stored as 0x3FC999999999999A
   = 0.200000000000000011102230246251565404236316680908203125
0.3 is stored as 0x3FD3333333333333
   = 0.299999999999999988897769753748434595763683319091796875
0.1 + 0.2 is stored as 0x3FD3333333333334
   = 0.3000000000000000444089209850062616169452667236328125
repr 0.30000000000000004, equal to 0.3: False
exact sum of the stored values, before rounding:
   = 0.3000000000000000166533453693773481063544750213623046875
halfway between 0x3FD3333333333333 and 0x3FD3333333333334: True
their significands: 5404319552844595 (odd) and 5404319552844596 (even)
ties to even gives 0x3FD3333333333334, one step (5.551115123125783e-17) above the double for 0.3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 point_three.py

The exact sum of the two stored values is 0.30000000000000001665…, which sits exactly halfway between 0x3FD3333333333333 (the double for 0.3) and the next double up, 0x3FD3333333333334. A tie like this is broken by the default rule, round to nearest, ties to even: of the two significands, 5404319552844595 and 5404319552844596, the even one wins, so the sum rounds up and misses 0.3 by one Unit in the last place: the gap between a floating-point number and the next larger one of its type; it grows with the size of the number., about 5.6 × 10⁻¹⁷. Intel’s manual, Arm’s and the RISC-V spec all name round to nearest, ties to even as the default rounding (floating-point): Choosing the representable value nearest to a result that cannot be stored exactly. The default IEEE 754 mode is round to nearest, ties to even: the closest value wins, and an exact tie goes to the one whose last significand bit is 0. mode. Follow the two roundings one stage at a time:

Interactive · stage by stage Why 0.1 + 0.2 rounds past 0.3 Follow each tenth as it is stored, added and rounded a second time.
Why 0.1 + 0.2 rounds past 0.3, step 5 of 5. Showing store 0.1 and 0.2, add the two doubles, sum is a tie, ties to even, 0x3FD3333333333334. In focus: 0x3FD3333333333334.store 0.1 and 0.2each rounded onceadd the two doublesan exact 106-bit sumsum is a tiehalfway between two doublesties to evenpicks the even significand0x3FD3333333333334= 0.30000000000000004

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Store each tenth. 0.1 becomes 0x3FB999999999999A and 0.2 becomes 0x3FC999999999999A. Neither is exact: a tenth repeats for ever in binary, so each is the nearest double, a hair too large.
  2. Add the stored values. Adding the two doubles gives 0.30000000000000001665…, the exact sum of what was really stored. This sum needs more than 52 fraction bits, so it cannot be a double as it stands.
  3. The sum lands on a tie. That exact sum sits exactly halfway between 0x3FD3333333333333 (the double for 0.3) and the next double up, 0x3FD3333333333334. The processor must pick one.
  4. Ties go to even. The default rule breaks a tie toward the even significand. Of 5404319552844595 and 5404319552844596, the even one is the larger, so the sum rounds up.
  5. One step past 0.3. The result is 0x3FD3333333333334, printed as 0.30000000000000004, one unit in the last place above the double for 0.3. So 0.1 + 0.2 == 0.3 is false.

Comparing floats with ==

if x == 0.3 fails for x = 0.1 + 0.2, because the stored sum is one step above the stored 0.3. Compare with a tolerance instead: in Python, math.isclose(a, b); by hand, test that abs(a - b) is below a small bound you choose for the size of the numbers. Never assume two routes to “the same” decimal give the same bits.

Checkpoint

Check: Rounding and 0.1 + 0.2

  1. Question 1 of 2 Why is 0.1 + 0.2 == 0.3 false in binary64?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Each literal rounds to the nearest double, and the rounded sum is one step above the double for 0.3

    0.1 stores as 0x3FB999999999999A and 0.2 as 0x3FC999999999999A, both slightly high. Their exact sum lands halfway between two doubles and rounds to 0x3FD3333333333334, one ulp above the 0x3FD3333333333333 that 0.3 stores as.

  2. Question 2 of 2 A result falls exactly halfway between two representable doubles. Under the default IEEE 754 mode, which one is chosen?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The one whose last significand bit is 0 (ties to even)

    Round to nearest, ties to even is the default on x86-64, AArch64 and RISC-V. For 0.1 + 0.2 the even significand is the larger one, so the sum rounds up and misses 0.3.

Zero, infinity, NaN and the subnormals

Two exponent fields are special. When the exponent field is all zeros the leading 1. is dropped and the number is 0.fraction × 2^(−1022): that gives two zeros (+0.0 and −0.0, differing only in the sign bit) and the subnormal number: A floating-point value with a zero exponent field and a non-zero fraction. Its implied leading bit is 0 instead of 1 and its exponent stays at the smallest normal one, −1022 for binary64, so subnormals fill the gap between the smallest normal number and zero with gradually fewer significant bits. numbers, which fill the gap between the smallest normal number and zero. When the exponent field is all ones, a zero fraction means an infinity (floating-point): An IEEE 754 value with the maximum exponent and a zero fraction, one for each sign. It is the result of overflow or of dividing a non-zero number by zero, and arithmetic with it follows defined rules, such as 1 divided by infinity being 0. (sign chooses which end) and a non-zero fraction means a NaN: Not a Number: an IEEE 754 pattern with the maximum exponent and a non-zero fraction, the result of operations such as 0.0/0.0 or infinity minus infinity. A NaN is not equal to anything, including itself, so x != x detects one., “not a number”, the result of 0.0/0.0 or inf − inf.

This program reads the bit pattern of each special value and shows what arithmetic does at the edges:

Zeros, subnormals, infinities and NaNs in binary64 Python · special/special.py
"""The special bit patterns of binary64: zeros, subnormals, infinities and NaNs, and what arithmetic does at the edges."""
import math
import struct
import sys


def bits(x):
    return struct.unpack("<Q", struct.pack("<d", x))[0]


def from_bits(b):
    return struct.unpack("<d", struct.pack("<Q", b))[0]


def show(name, x):
    b = bits(x)
    exponent = (b >> 52) & 0x7FF
    fraction = b & ((1 << 52) - 1)
    print(f"{name:<19} {b:016X}  exp {exponent:>4}  frac {'0' if fraction == 0 else 'not 0':<5}  {x!r}")


inf = math.inf
show("+0.0", 0.0)
show("-0.0", -0.0)
show("smallest subnormal", from_bits(1))
show("largest subnormal", from_bits(0x000FFFFFFFFFFFFF))
show("smallest normal", sys.float_info.min)
show("1.0", 1.0)
show("largest finite", sys.float_info.max)
show("+infinity", inf)
show("-infinity", -inf)
show("inf - inf", inf - inf)  # an invalid operation, computed by this machine's floating-point unit
show("quiet bit clear", from_bits(0x7FF0000000000001))
show("sign bit set", from_bits(0xFFF8000000000000))

print()
print("-0.0 == 0.0:", -0.0 == 0.0, "| copysign(1, -0.0):", math.copysign(1, -0.0))
nan = inf - inf
print("nan == nan:", nan == nan, "| nan != nan:", nan != nan, "| isnan:", math.isnan(nan))
print("largest finite * 2:", sys.float_info.max * 2, "| isinf:", math.isinf(sys.float_info.max * 2))
print("smallest normal / 2**52:", sys.float_info.min / 2**52, "| / 2**53:", sys.float_info.min / 2**53)
print("1 / inf:", 1 / inf, "| -1 / inf:", -1 / inf)

Output

+0.0                0000000000000000  exp    0  frac 0      0.0
-0.0                8000000000000000  exp    0  frac 0      -0.0
smallest subnormal  0000000000000001  exp    0  frac not 0  5e-324
largest subnormal   000FFFFFFFFFFFFF  exp    0  frac not 0  2.225073858507201e-308
smallest normal     0010000000000000  exp    1  frac 0      2.2250738585072014e-308
1.0                 3FF0000000000000  exp 1023  frac 0      1.0
largest finite      7FEFFFFFFFFFFFFF  exp 2046  frac not 0  1.7976931348623157e+308
+infinity           7FF0000000000000  exp 2047  frac 0      inf
-infinity           FFF0000000000000  exp 2047  frac 0      -inf
inf - inf           7FF8000000000000  exp 2047  frac not 0  nan
quiet bit clear     7FF0000000000001  exp 2047  frac not 0  nan
sign bit set        FFF8000000000000  exp 2047  frac not 0  nan

-0.0 == 0.0: True | copysign(1, -0.0): -1.0
nan == nan: False | nan != nan: True | isnan: True
largest finite * 2: inf | isinf: True
smallest normal / 2**52: 5e-324 | / 2**53: 0.0
1 / inf: 0.0 | -1 / inf: -0.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 special.py

The smallest positive subnormal is 0x0000000000000001, about 5 × 10⁻³²⁴; the largest finite double is 0x7FEFFFFFFFFFFFFF, about 1.8 × 10³⁰⁸, and doubling it gives inf. A NaN has a quiet bit, the top fraction bit: a quiet NaN (0x7FF8…) has it set and flows through arithmetic, while a signalling NaN (0x7FF0… with a non-zero rest) has it clear and is meant to raise an exception. The run above returns 0x7FF8000000000000 for inf − inf; which NaN an operation delivers is the processor’s choice, not the standard’s, so the exact bits can differ between x86-64, AArch64 and RISC-V.

NaN is never equal to anything

nan == nan is false, and so is every other comparison with a NaN, which is how math.isnan(x) can be written as x != x. A sort that assumes a total order can loop or drop values when a NaN slips in, so filter NaNs out before you sort floating-point data.

Checkpoint

Check: Zeros, infinities, NaNs and subnormals

  1. Question 1 of 2 A binary64 pattern has an exponent field of all ones and a fraction of all zeros. What is it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An infinity (positive or negative by the sign bit)

    An all-ones exponent means infinity (zero fraction) or NaN (non-zero fraction). An all-zero exponent means zero (zero fraction) or a subnormal (non-zero fraction).

  2. Question 2 of 2 Two NaNs are 0x7FF8000000000000 and 0x7FF4000000000000. Which is the quiet NaN?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 0x7FF8000000000000, because its top fraction bit is set

    A quiet NaN has the most significant fraction bit set and flows through arithmetic; a signalling NaN has it clear and is meant to trap. 0x7FF8… is quiet, 0x7FF4… is signalling.

Moving the bits to an integer register

Each ISA keeps floating-point values in their own register file, apart from the integer registers: x86-64 in the SSE registers xmm0–xmm15, AArch64 in the 128-bit vector registers v0–v31 (named d0–d31 for a 64-bit double and s0–s31 for a 32-bit float), and RISC-V in f0–f31 through the F and D extensions. To inspect the bits with the integer instructions you already know, you copy them across without converting the value. That is one instruction per ISA: movq rax, xmm0 on x86-64, fmov x0, d0 on AArch64 and fmv.x.d a0, fa0 on RISC-V. Each moves 64 bits as they are; none rounds or changes a single bit.

Step through the AArch64 copy, and see how it differs from a conversion:

Interactive · step by step fmov copies the bits, fcvtzs converts Watch the 64 bits move from d0 to x0 unchanged, then see how fcvtzs differs.
fmov copies the bits, fcvtzs converts, step 4 of 4. Showing d0, x0, and, lsr, fcvtzs x0, d0. In focus: fcvtzs x0, d0.fmov x0, d0d0holds the bitsx0same 64 bitsand, lsrread the fieldsfcvtzs x0, d0converts, not copies

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Bits in d0. d0 is the 64-bit view of vector register v0, and it holds the bits of a double, say 0x3FB999999999999A for 0.1. The floating-point unit put them there.
  2. Copy them across. fmov x0, d0 copies all 64 bits into the integer register x0 without conversion. It assembles to the four bytes 9e660000. The value is unchanged; only the register file it sits in is different.
  3. Read the fields. Now the integer instructions can work on the pattern: lsr and and shift and mask out the sign, exponent and fraction, exactly as the decode program did in software.
  4. Converting is different. fcvtzs x0, d0 (bytes 9e780000) is not a copy: it converts the value to an integer, rounding toward zero, so 0.1 becomes 0. Reach for fmov when you want the bits, fcvtzs when you want the number.

The AArch64 program below is three such functions. A C caller passes a double in d0, and fmov x0, d0 returns its bits in x0; fmov d0, x0 goes the other way; fcvtzs x0, d0, by contrast, converts, rounding the value toward zero to an integer.

bits_of.s (AArch64, called from C on macOS) S · asm/bits_of.s
// bits_of.s: three AArch64 functions for macOS that a C program calls. Under AAPCS64 a double argument arrives in
// d0 (the low 64 bits of v0), an integer argument in x0, an integer result leaves in x0 and a double result in d0.
        .text
        .globl  _bits_of, _from_bits, _to_integer
        .p2align 2

_bits_of:                       // uint64_t bits_of(double x)
        fmov    x0, d0          // copy the 64 bits to an integer register, without conversion
        ret

_from_bits:                     // double from_bits(uint64_t b)
        fmov    d0, x0          // the same copy, the other way
        ret

_to_integer:                    // int64_t to_integer(double x)
        fcvtzs  x0, d0          // a conversion: the value, rounded toward zero
        ret

Assembled with a small C caller, linked and run on an Apple silicon Mac:

fmov moves bits, fcvtzs converts: AArch64, run on a Mac Python · bits_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# AArch64 on an Apple silicon Mac: assemble bits_of.s, link it with main.c, run it, then read the bytes of fmov.
session.run("""
clang -c -o bits_of.o asm/bits_of.s
clang -o bits asm/main.c bits_of.o
./bits; echo "exit=$?"
objdump -d --disassemble-symbols=_bits_of,_from_bits,_to_integer bits_of.o
""")

Output

$ clang -c -o bits_of.o asm/bits_of.s
$ clang -o bits asm/main.c bits_of.o
$ ./bits; echo "exit=$?"
0.10000000000000001  bits 0x3FB999999999999A  integer 0
-6.25                bits 0xC019000000000000  integer -6
0.30000000000000004  bits 0x3FD3333333333334  integer 0
from_bits(0x3FD3333333333333) = 0.29999999999999999
exit=0
$ objdump -d --disassemble-symbols=_bits_of,_from_bits,_to_integer bits_of.o

bits_of.o:	file format mach-o arm64

Disassembly of section __TEXT,__text:

0000000000000000 <_bits_of>:
       0: 9e660000     	fmov	x0, d0
       4: d65f03c0     	ret

0000000000000008 <_from_bits>:
       8: 9e670000     	fmov	d0, x0
       c: d65f03c0     	ret

0000000000000010 <_to_integer>:
      10: 9e780000     	fcvtzs	x0, d0
      14: d65f03c0     	ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bits_session.py

The disassembly shows fmov x0, d0 as the four bytes 9e660000 and fcvtzs x0, d0 as 9e780000: different instructions, one a copy and one a conversion. The run prints 0.1 as 0x3FB999999999999A with integer 0, and 0.1 + 0.2 as 0x3FD3333333333334, the pattern from the section above. The same idea on the other two ISAs is assembled into Linux objects, which the session does not run; the assembler only encodes each instruction into its bytes:

movq and fmv.x.d encoded for x86-64 and RISC-V Python · encodings_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# Nothing here runs x86-64 or RISC-V code: LLVM assembles both files into Linux ELF objects and prints the bytes
# each instruction became. llvm-mc and llvm-objdump come from Homebrew's LLVM, which knows RISC-V.
session.run("""
clang --target=x86_64-linux-gnu -c -o bits_x86_64.o asm/bits_of_x86_64.s
objdump -d --x86-asm-syntax=intel bits_x86_64.o
llvm-mc --triple=riscv64 -mattr=+d -filetype=obj -o bits_riscv64.o asm/bits_of_riscv64.s
llvm-objdump -d bits_riscv64.o
""")

Output

$ clang --target=x86_64-linux-gnu -c -o bits_x86_64.o asm/bits_of_x86_64.s
$ objdump -d --x86-asm-syntax=intel bits_x86_64.o

bits_x86_64.o:	file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <bits_of>:
       0: 66 48 0f 7e c0               	movq	rax, xmm0
       5: c3                           	ret

0000000000000006 <from_bits>:
       6: 66 48 0f 6e c7               	movq	xmm0, rdi
       b: c3                           	ret
$ llvm-mc --triple=riscv64 -mattr=+d -filetype=obj -o bits_riscv64.o asm/bits_of_riscv64.s
$ llvm-objdump -d bits_riscv64.o

bits_riscv64.o:	file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <bits_of>:
       0: e2050553     	fmv.x.d	a0, fa0
       4: 00008067     	ret

0000000000000008 <from_bits>:
       8: f2050553     	fmv.d.x	fa0, a0
       c: 00008067     	ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 encodings_session.py

bits_of_x86_64.s (x86-64, Intel syntax) S · asm/bits_of_x86_64.s
# bits_of_x86_64.s: the x86-64 Linux version, in Intel syntax for GNU as or LLVM. Under the System V ABI a double
# argument arrives in xmm0, the first integer argument in rdi, an integer result leaves in rax, a double in xmm0.
        .intel_syntax noprefix
        .text
        .globl  bits_of, from_bits

bits_of:                        # uint64_t bits_of(double x)
        movq    rax, xmm0       # copy the low 64 bits of xmm0 to rax, without conversion
        ret

from_bits:                      # double from_bits(uint64_t b)
        movq    xmm0, rdi       # the same copy, the other way
        ret

        .section .note.GNU-stack,"",@progbits
bits_of_riscv64.s (RISC-V, LP64D) S · asm/bits_of_riscv64.s
# bits_of_riscv64.s: the RISC-V version for RV64 with the D extension, on Linux. Under the LP64D calling convention a
# double argument arrives in fa0 (f10), an integer argument in a0 (x10), an integer result leaves in a0, a double in fa0.
        .text
        .globl  bits_of, from_bits

bits_of:                        # uint64_t bits_of(double x)
        fmv.x.d a0, fa0         # copy the 64 bits of fa0 to a0, unchanged
        ret

from_bits:                      # double from_bits(uint64_t b)
        fmv.d.x fa0, a0         # the same copy, the other way
        ret

        .section .note.GNU-stack,"",@progbits

On x86-64 the System V ABI hands a double to a function in xmm0 and returns an integer in rax, so movq rax, xmm0 (bytes 66 48 0f 7e c0) reads the bits; Intel’s manual lists this 66 REX.W 0F 7E /r form as “Move quadword from xmm register to r/m64”. On RISC-V the LP64D convention passes a double in fa0 and returns in a0, so fmv.x.d a0, fa0 (bytes e2050553) reads them, and the RISC-V spec notes these moves “do not modify the bits being transferred”. The move instruction only relabels the register file the bits live in.

Checkpoint

Check: Moving bits to an integer register

  1. Question 1 of 2 Which instruction copies a double's 64 bits into an integer register on AArch64, without converting the value?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: fmov x0, d0

    fmov x0, d0 moves the 64 bits of d0 into x0 without conversion. The x86-64 form is movq rax, xmm0 and the RISC-V form is fmv.x.d a0, fa0.

  2. Question 2 of 2 A double in d0 holds 0.1. After fmov x0, d0, what does x0 hold?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 0x3FB999999999999A, the exact bit pattern of 0.1

    fmov copies the pattern, so x0 becomes 0x3FB999999999999A. fcvtzs x0, d0 would instead convert 0.1 to the integer 0.

How wide is a long double?

The three fields scale up to a fourth IEEE format, binary128, with a 15-bit exponent and a 112-bit fraction, and down to binary16, the half precision used in machine learning. The C type long double, though, is where the platforms part ways, and it is an ABI choice, not an IEEE one. This program prints the width on the machine that runs it, then asks three Linux compilers what they would use:

sizeof(long double) on macOS and three Linux targets Python · ldsize_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# long double on the host (run), then what clang's own description of three Linux targets says about it (no run:
# -dM -E prints the macros the compiler predefines for a target). clang-23 is Homebrew's LLVM, which knows RISC-V.
session.run("""
clang -o ldsize c/ldsize.c
./ldsize; echo "exit=$?"
clang --target=aarch64-linux-gnu -dM -E -x c /dev/null | grep -E 'SIZEOF_LONG_DOUBLE|LDBL_MANT_DIG' | sort
clang --target=x86_64-linux-gnu -dM -E -x c /dev/null | grep -E 'SIZEOF_LONG_DOUBLE|LDBL_MANT_DIG' | sort
clang-23 --target=riscv64-linux-gnu -dM -E -x c /dev/null | grep -E 'SIZEOF_LONG_DOUBLE|LDBL_MANT_DIG' | sort
""")

Output

$ clang -o ldsize c/ldsize.c
$ ./ldsize; echo "exit=$?"
double:      8 bytes, 53 significand bits
long double: 8 bytes, 53 significand bits
exit=0
$ clang --target=aarch64-linux-gnu -dM -E -x c /dev/null | grep -E 'SIZEOF_LONG_DOUBLE|LDBL_MANT_DIG' | sort
#define __LDBL_MANT_DIG__ 113
#define __SIZEOF_LONG_DOUBLE__ 16
$ clang --target=x86_64-linux-gnu -dM -E -x c /dev/null | grep -E 'SIZEOF_LONG_DOUBLE|LDBL_MANT_DIG' | sort
#define __LDBL_MANT_DIG__ 64
#define __SIZEOF_LONG_DOUBLE__ 16
$ clang-23 --target=riscv64-linux-gnu -dM -E -x c /dev/null | grep -E 'SIZEOF_LONG_DOUBLE|LDBL_MANT_DIG' | sort
#define __LDBL_MANT_DIG__ 113
#define __SIZEOF_LONG_DOUBLE__ 16

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ldsize_session.py

On macOS on Apple silicon a long double is 8 bytes with 53 significand bits: Apple’s ARM64 document records that it is “identical to the double type”. The three Linux ABIs all make it 16 bytes, but store different things inside: AArch64 and RISC-V hold true binary128 (113 significand bits), as AAPCS64 and the RISC-V psABI require, while x86-64 holds the old x87 80-bit extended format (64 significand bits), whose 10 significant bytes sit in 16 bytes of storage with 6 bytes of padding, as the x86-64 psABI sets out. The table gathers what the run and the ABI documents say:

Target sizeof(long double) Significand bits Format
macOS AArch64 (the run above) 8 53 same as double
Linux AArch64 16 113 IEEE binary128
Linux RISC-V (RV64) 16 113 IEEE binary128
Linux x86-64 16 64 x87 80-bit extended

Key idea

double and float are the same bit layout everywhere, but long double is an ABI decision: 8 bytes on an Apple silicon Mac, 16 bytes on the Linux ABIs, holding binary128 on AArch64 and RISC-V but the 80-bit x87 format on x86-64.

Checkpoint

Check: How wide a long double is

  1. Question 1 of 2 On an Apple silicon Mac, how large is a long double, and what format does it use?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 8 bytes, the same as a double

    On macOS AArch64 long double is 8 bytes, identical to double. The Linux ABIs make it 16 bytes: binary128 on AArch64 and RISC-V, the 80-bit x87 format on x86-64.

  2. Question 2 of 2 IEEE 754 binary128 has a 15-bit exponent and a 112-bit stored fraction. Counting the implied leading 1, how many significand bits does a normal value have?

    Type a number.

    A hint

    A normal number has one implied leading bit on top of the stored fraction bits.

    Show the answer: Question 2 of 2

    Answer: 113 bits

    The stored fraction is 112 bits and the leading 1 is implied, so the significand is 113 bits. That is what Linux AArch64 and RISC-V give a long double; binary64 by the same count has 52 + 1 = 53.

Practise: is a bit pattern a NaN?

Write is_nan_bits, which decides whether a 64-bit pattern is a NaN using only integer operations, the way an assembly routine would with fmov and a mask: a pattern is a NaN exactly when its exponent field is all ones and its fraction is non-zero. The sample tests cover a quiet NaN, a signalling NaN, both infinities, zero and ordinary numbers:

Exercise · Easy · Python

Tell a NaN from its bits, with integer operations

Write is_nan_bits(bits) in is_nan_bits.py.

bits is a whole number from 0 to 2⁶⁴ − 1: the raw bit pattern of an IEEE 754 binary64 value, as fmov x0, d0 would leave it in an integer register. Return True when the pattern is a NaN and False otherwise, using only integer operations (shifts, masks and comparisons) — never turn the pattern back into a float.

A binary64 pattern is a NaN exactly when both of these hold:

  • its 11 exponent bits (bits 52 to 62) are all ones, that is, the exponent field equals 0x7FF;
  • its 52 fraction bits (bits 0 to 51) are not all zero.

An all-ones exponent with a zero fraction is an infinity, not a NaN, so it must return False. The sign bit (bit 63) does not matter: a negative NaN is still a NaN. All the tests are public: they cover a quiet NaN, a signalling NaN, both infinities, the zeros, a subnormal, ordinary numbers and the largest finite double.

Starter code · is_nan_bits.py

def is_nan_bits(bits):
    """True when the 64-bit binary64 pattern `bits` is a NaN, using integer operations only.

    bits: a whole number from 0 to 2**64 - 1 (the raw bits of a double).
    """
    raise NotImplementedError("write is_nan_bits")
The sample tests · test_is_nan_bits.py
from is_nan_bits import is_nan_bits


def test_quiet_nan():
    """a quiet NaN: exponent all ones, top fraction bit set"""
    assert is_nan_bits(0x7FF8000000000000) is True


def test_signalling_nan():
    """a signalling NaN: exponent all ones, a low fraction bit set, quiet bit clear"""
    assert is_nan_bits(0x7FF0000000000001) is True


def test_negative_nan():
    """the sign bit does not matter: a negative NaN is still a NaN"""
    assert is_nan_bits(0xFFF8000000000000) is True


def test_positive_infinity():
    """exponent all ones but fraction zero is +infinity, not a NaN"""
    assert is_nan_bits(0x7FF0000000000000) is False


def test_negative_infinity():
    """exponent all ones, fraction zero, sign set: -infinity, not a NaN"""
    assert is_nan_bits(0xFFF0000000000000) is False


def test_zero():
    """+0.0 is all zero bits"""
    assert is_nan_bits(0x0000000000000000) is False


def test_negative_zero():
    """-0.0 has only the sign bit set"""
    assert is_nan_bits(0x8000000000000000) is False


def test_one():
    """1.0 = 0x3FF0000000000000, an ordinary number"""
    assert is_nan_bits(0x3FF0000000000000) is False


def test_subnormal():
    """the smallest positive subnormal, exponent zero and a single fraction bit"""
    assert is_nan_bits(0x0000000000000001) is False


def test_largest_finite():
    """the largest finite double: exponent 0x7FE (not all ones), fraction all ones"""
    assert is_nan_bits(0x7FEFFFFFFFFFFFFF) is False
A hint

Pull the exponent field out with a shift and a mask: exponent = (bits >> 52) & 0x7FF. Pull the fraction out with fraction = bits & ((1 << 52) - 1). The pattern is a NaN when the exponent equals 0x7FF and the fraction is not zero. Return that single boolean; do not build a float.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

IEEE 754 Floating-Point Converter Flip the exponent and fraction bits of a double and watch a pattern turn into a NaN, an infinity or a subnormal.

The questions below cover the whole lesson, weakest idea first and harder as your answers come out right:

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 A binary64 value has sign 0, real exponent 1 (after removing the bias) and significand 1.5. What value is it?

    Type a number.

    A hint

    The value is (-1)^sign × significand × 2^exponent.

    Show the answer: Question 1 of 11

    Answer: 3

    1.5 × 2^1 = 3.0, with a positive sign. Its bit pattern is 0x4008000000000000: exponent field 1024, fraction a single top bit.

  2. Question 2 of 11 Why does IEEE 754 store the exponent with a bias added, instead of as a two's complement signed number?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: So the bit patterns of larger numbers compare as larger unsigned integers, which makes ordering and sorting simple

    With a bias, a larger real exponent is a larger stored field, so comparing two same-signed floats as integers gives the right order. This is the basis of the sortable-integer-key trick.

  3. Question 3 of 11 Why can a double not store 0.1 exactly?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: 0.1 is a fraction that repeats for ever in binary, so no finite fraction field holds it

    Only fractions whose denominator is a power of two are exact in binary. 0.1 is 1/10, so it repeats, and the stored double is the nearest value, a little above a tenth.

  4. Question 4 of 11 You must check whether two computed doubles a and b are "the same". What is the robust test?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Test that abs(a - b) is within a small tolerance chosen for the size of the numbers

    Because each operation rounds, values that are mathematically equal can differ in their last bits. Compare with a tolerance, such as math.isclose in Python, not with ==.

  5. Question 5 of 11 A binary64 pattern has an all-zero exponent field and a non-zero fraction. What is it?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: A subnormal number

    All-zero exponent, non-zero fraction is a subnormal. All-zero exponent, zero fraction is a zero; all-ones exponent splits into infinity (zero fraction) and NaN (non-zero fraction).

  6. Question 6 of 11 A list of doubles may contain a NaN. What goes wrong if you sort it with an ordinary comparison, and what do you do?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: NaN compares false against everything, so the order is undefined and a sort can misplace or drop values; filter NaNs out first

    NaN is unordered: x < nan, x > nan and x == nan are all false. A comparison sort relies on a total order, so a NaN can break it silently. Filter NaNs out, or use a key that places them deliberately.

  7. Question 7 of 11 Why keep floating-point values in their own register file (xmm, v or f registers) rather than the integer registers?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: The floating-point unit acts on those registers, and moving bits to an integer register is one explicit instruction when you want to inspect them

    The FP registers feed the floating-point unit. To read a value's bits with integer instructions you copy them across with one move; to use the number as an integer you convert instead.

  8. Question 8 of 11 On RISC-V, the double in fa0 is to be read as bits in a0 and then its sign bit masked out. Which pair of steps is right?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: fmv.x.d a0, fa0 to copy the 64 bits, then clear bit 63 with an integer mask

    fmv.x.d a0, fa0 copies the 64 bits into a0 without conversion, and then an integer AND clears the sign bit. The equivalents are movq on x86-64 and fmov on AArch64.

  9. Question 9 of 11 C code prints sizeof(long double) and gets 8 on a Mac and 16 on a Linux server. Is this a bug?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: No: long double is an ABI choice, 8 bytes (same as double) on Apple silicon and 16 bytes on the Linux ABIs

    long double is 8 bytes on an Apple silicon Mac (identical to double) and 16 bytes on the Linux ABIs, holding binary128 on AArch64 and RISC-V and the 80-bit x87 format on x86-64. Portable code should not assume its size.

  10. Question 10 of 11 Roughly how many significant decimal digits does a binary32 float keep, and why fewer than a double?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: About 7, because it has 24 significand bits against a double''s 53

    A float's 24 significand bits are about 24 × log10 2 ≈ 7.2 decimal digits; a double's 53 bits are about 15.95. That is why 0.1 stored as a float, 0.10000000149, is wrong sooner than as a double.

  11. Question 11 of 11 On the Linux AArch64, x86-64 and RISC-V ABIs, how many bytes does a long double take?

    Type a number.

    A hint

    The three Linux ABIs agree on the size, even though they differ on the format inside it.

    Show the answer: Question 11 of 11

    Answer: 16 bytes

    All three Linux ABIs make a long double 16 bytes, though AArch64 and RISC-V put binary128 inside while x86-64 puts the 80-bit x87 format in 10 of the 16 bytes. On an Apple silicon Mac a long double is only 8 bytes.

Interview questions

How is a double laid out in its 64 bits, and how do you read the value back?

A binary64 double is one sign bit, then 11 exponent bits, then 52 fraction bits. A normal value is (-1)^sign × 1.fraction × 2^(exponent − 1023): the significand has a leading 1 that is implied and not stored, and the exponent is held with a bias of 1023 added, so a stored field of 1025 means an exponent of 2. Two exponent fields are special: all zeros gives the zeros and the subnormal numbers (no leading 1, exponent fixed at −1022), and all ones gives the infinities (zero fraction) and the NaNs (non-zero fraction). To read a value back you take the bytes as a 64-bit integer, shift and mask out the three fields, undo the bias, and put the leading 1 back for a normal number. A float is the same shape with an 8-bit exponent biased by 127 and a 23-bit fraction. The sign is always the top bit, so a negative number differs from its magnitude in one bit.

Two harder questions follow: one sorts doubles by reading them as integers, the other chases an error that grows over a long running total. Think each through before you open it:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    You want to sort a large array of doubles by reinterpreting each as an unsigned 64-bit integer and sorting those integers (for a radix sort, or a byte-ordered database key). A plain reinterpret-cast does not sort correctly. What transform fixes it, and what happens to negatives, the two zeros and NaNs?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A dashboard sums millions of small positive readings into one double, and the total drifts away from a slower exact sum. Why does a long running total lose accuracy, and what are two ways to make it more accurate without changing the stored data type?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are the three fields of a binary64 double, and how is its value read back?

    A sign bit, an 11-bit exponent biased by 1023 and a 52-bit fraction. A normal value is (-1)^sign x 1.fraction x 2^(exponent - 1023), with the leading 1 implied and never stored.

  2. How does binary32, the C float, differ from binary64?

    Same shape, smaller fields: an 8-bit exponent biased by 127 and a 23-bit fraction after the sign bit, so 24 bits of significand instead of 53.

  3. Why is 0.1 + 0.2 not equal to 0.3 in doubles?

    None of the three values is exact in binary, so each is rounded to the nearest double when stored. The sum comes out as 0x3FD3333333333334, which is one ulp higher than the stored 0.3. Compare floating-point values with a tolerance.

  4. Which exponent patterns give zeros, subnormals, infinities and NaNs?

    An all-zero exponent gives the zeros (fraction 0) and the subnormals (fraction non-zero). An all-one exponent gives the infinities (fraction 0) and the NaNs (fraction non-zero). A quiet NaN has the top fraction bit set; a signalling NaN has it clear.

  5. How can code detect a NaN without looking at its bits?

    Test x != x. A NaN compares unequal to everything, itself included, while every other value equals itself.

  6. How do you copy the raw 64 bits of a double into an integer register on each ISA?

    movq rax, xmm0 on x86-64, fmov x0, d0 on AArch64 and fmv.x.d a0, fa0 on RISC-V. They copy the bits unchanged; fcvtzs and its kin convert the value to an integer instead.

  7. How wide is long double?

    It is an ABI choice. On an Apple silicon Mac it is 8 bytes, the same as double. The Linux ABIs give it 16 bytes, holding binary128 on AArch64 and RISC-V and the old x87 80-bit extended format on x86-64.

Key takeaways

  • A binary64 double is a sign bit, an 11-bit exponent biased by 1023 and a 52-bit fraction, read as (-1)^sign × 1.fraction × 2^(exponent − 1023); binary32 is the same shape with an 8-bit exponent biased by 127 and a 23-bit fraction.
  • 0.1, 0.2 and 0.3 each round to the nearest double when stored, so 0.1 + 0.2 is 0x3FD3333333333334, one unit in the last place above the double for 0.3; compare floats with a tolerance, never ==.
  • An all-zero exponent gives the zeros and subnormals; an all-one exponent gives the infinities (zero fraction) and the NaNs (non-zero fraction), and a quiet NaN has the top fraction bit set while a signalling NaN has it clear.
  • movq rax, xmm0, fmov x0, d0 and fmv.x.d a0, fa0 copy 64 bits from a floating-point register to an integer one without converting, so you can inspect them; fcvtzs and its kin convert the value instead.
  • double and float have the same layout everywhere, but long double is an ABI choice: 8 bytes on an Apple silicon Mac, and 16 bytes on the Linux ABIs holding binary128 on AArch64 and RISC-V but the 80-bit x87 format on x86-64.

Terms in this lesson

exponent bias
A constant added to the real exponent so the stored exponent field is a non-negative integer that still covers negative powers. binary64 adds 1023 and binary32 adds 127, so a stored field equal to the bias means an exponent of 0.
floating-point number
A number stored as a sign, an exponent and a fraction, so one format covers a wide range by moving a binary point. binary32 (C float) and binary64 (C double) are the common sizes; the value is roughly 1.fraction times 2 to the exponent.
IEEE 754
The standard for floating-point arithmetic that x86-64, AArch64 and RISC-V all follow. It fixes the binary16, binary32, binary64 and binary128 formats, how each lays out a sign, exponent and fraction, and how results are rounded.
infinity (floating-point)
An IEEE 754 value with the maximum exponent and a zero fraction, one for each sign. It is the result of overflow or of dividing a non-zero number by zero, and arithmetic with it follows defined rules, such as 1 divided by infinity being 0.
NaN
Not a Number: an IEEE 754 pattern with the maximum exponent and a non-zero fraction, the result of operations such as 0.0/0.0 or infinity minus infinity. A NaN is not equal to anything, including itself, so x != x detects one.
rounding (floating-point)
Choosing the representable value nearest to a result that cannot be stored exactly. The default IEEE 754 mode is round to nearest, ties to even: the closest value wins, and an exact tie goes to the one whose last significand bit is 0.
significand
The fraction part of a floating-point number, the digits of its value apart from the exponent. For a normal number it is 1.fraction with a leading 1 that is implied and not stored; binary64 keeps 53 bits of it, binary32 keeps 24.
subnormal number
A floating-point value with a zero exponent field and a non-zero fraction. Its implied leading bit is 0 instead of 1 and its exponent stays at the smallest normal one, −1022 for binary64, so subnormals fill the gap between the smallest normal number and zero with gradually fewer significant bits.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.