====== Floating Point Explained ====== As an extension to any internal documentation of PDCLib, and because being able to //explain// a subject is a good test whether I myself have //understood// the subject, I'll give an introduction to floating point format here. ===== Base-10 Basics ===== We will take a short detour through numeric notations to lay out the terminology. ==== Decimal Notation ==== [[https://en.wikipedia.org/wiki/Decimal_notation | Decimal notation]] uses the digits 0 through 9, which carry value according to their position. Consider the decimal number 123.4: ^ Digit ^ 1 ^ 2 ^ 3 ^ . ^ 4 ^ ^ Value | *10^2 (100) | *10^1 (20) | *10^0 (3) | | *10^-1 (0.4) | ==== Scientific Notation ==== Decimal notation gets more and more unwieldy the further your significant digits are away from the decimal point -- very large, or very small numbers. For these, you would use [[https://en.wikipedia.org/wiki/Scientific_notation | scientific notation]], where a number is written as m*10^n. The m is the [[https://en.wikipedia.org/wiki/Significand | significand]], also called "mantissa" (which is the more common term when talking about floating point numbers in computing), while n is the [[https://en.wikipedia.org/wiki/Exponent | exponent]] and 10 is the [[https://en.wikipedia.org/wiki/Base_(exponentiation) | base]]. Let's return to our number 123.4, which could be written as 123.4*10^0. Any number to the zeroth power equals one, so multiplying by 10^0 is a no-op (and usually omitted, returning to normal //decimal notation//). Increment or decrement the exponent to shift the decimal point of the significand one digit to the left or the right, respectively. So we could also write 12.34*10^1, 1234*10^-1, or 1.234*10^2 -- the decimal point is "floating" along the significand. This allows us to write very large numbers like 1.234*10^78, or very small numbers like 1.234*10^-34 efficiently, by scaling the exponent appropriately. ==== Normalized Number ==== The two numbers in the previous paragraph were written so that there was only one significant digit in front of the decimal point. This is called a [[https://en.wikipedia.org/wiki/Normalized_number | normalized number]]. It allows for quick comparison of numbers as the exponent now tells us the //order of magnitude// at a glance. ===== Base-2 ===== Now that we had a look at //scientific notation// and know what a //normalized number// is, let us have a look at how a computer actually encodes floating point values on the binary level. ==== Binary Encoding ==== First thing, the //base// used by a computer is (usually) 2 instead of 10, for obvious reasons. It is implied, and does not need to be stored. One bit is used as //sign bit// for the mantissa (i.e. whether the whole number is positive or negative). A number of bits is set apart for the mantissa, and a number of bits is set apart for the exponent. For easier visualization, we will use a (theoretical) 6-bit notation: One sign bit, two mantissa bits, and three exponent bits. This allows us to have a look at every possible value. === Sign === Unset indicates positive, set negative. Yes, this format has a [[https://en.wikipedia.org/wiki/Signed_zero | signed zero]]. === Mantissa === The mantissa is assumed to be //normalized//, i.e. the highest (non-fractional) bit is assumed to be set. It has a value of 2^0, i.e. 1, with following fractional bits having incrementing negative exponents. Under this assumption, the highest bit need not actually be stored, giving room for an additional //fractional// mantissa bit at the end. Most people who read thus far can probably rattle down the values for //positive// base-2 exponents -- one, two, four, eight, sixteen and so on. //Negative// exponents usually give people a moment pause. But it's quite easy: While each positive increment //doubles// the value, each decrement //halves// it -- 0.5, 0.25, 0.125, 0.0625 and so on. === Exponent === The exponent is a funny bugger. IEEE 754 defines the exponent as an absolute (positive) offset to a (negative) bias. That bias is calculated as 1-2^(bits-1). "All bits clear" and "all bits set" are reserved (we will come to this later), so the smallest meaningful exponent is 1. For our 3-bit exponent, we get a bias of 1-2^2 = -3, with a practical range of -2..3: ^ Exponent ^ Value ^ ^ 000 | reserved | ^ 001 | -2 | ^ 010 | -1 | ^ 011 | 0 | ^ 100 | 1 | ^ 101 | 2 | ^ 110 | 3 | ^ 111 | reserved | ==== Exceptions ==== === Infinity === If all exponent bits are set and all mantissa bits are cleared, that indicates an "infinity" value. This usually happens as a result of overflow, or a division by zero. (**Note:** Division by zero does //not// give "infinity" as a result, mathematically. This is a quirk of the IEEE 754 floating point format!) Positive or negative infinites are indicated by the sign bit. === Not a Number === If all exponent bits are set and there are mantissa bits set, that indicates "not a number" (//NaN//). This can happen for certain operations like taking the square root of a negative value. A processor might support //signaling NaNs//, which lead to a processor exception when used in an operation. To indicate whether a //NaN// is "signalling" or "quiet", the highest mantissa bit is used, albeit in a processor-specific way. PA_RISC and old MIPS processors use that bit as ''is_signalling'', while most other processors use it as ''is_quiet''. The latter procedure allows to "silence" a //NaN// (by setting the bit) without inadvertently turning a //NaN// into //infinity//. The sign bit does not matter for //NaN// values. === Denormalized Numbers === When all exponent bits are cleared, the mantissa is considered to be [[https://en.wikipedia.org/wiki/Denormal_number | denormalized]], with an exponent of 1-(bias). In other words, the smallest representable exponent (with only the lowest bit set) is applied, //with the usually implied pre-fractional bit unset//. The purpose, here, is to allow a bit of additional precision in the very smallest numbers presentable. Using our theoretical 6-bit format: ^ Exponent ^ Mantissa (stored) ^ Mantissa (implied) ^ Value ^ | 001 | 11 | 1.11 | 1.75 * 2^(-2) = 0.4375 | | 001 | 10 | 1.10 | 1.50 * 2^(-2) = 0.375 | | 001 | 01 | 1.01 | 1.25 * 2^(-2) = 0.3125 | | 001 | 00 | 1.00 | 1.00 * 2^(-2) = 0.25 | | 000 | 11 | **0**.11 | **0**.75 * 2^(-2) = 0.1875 | | 000 | 10 | **0**.10 | **0**.50 * 2^(-2) = 0.125 | | 000 | 01 | **0**.01 | **0**.25 * 2^(-2) = 0.0625 | | 000 | 00 | **0**.00 | **0** | ==== Formats ==== === IEEE 754 === The [[https://en.wikipedia.org/wiki/IEEE_754 | IEEE 754]] standard defines various formats, of which basically only two are really of interest when looking at floating point support in a C standard library: Single and double precision. ^ Format ^ Single Precision ^ Double Precision ^ Quadruple Precision ^ ^ Overall Width | 32 bits | 64 bits | 128 bits | ^ Significand | 24 bits | 53 bits | 113 bits | ^ Exponent | 8 bits | 11 bits | 15 bits | ^ Exponent Bias | 127 | 1023 | 16383 | ^ E min | -126 | -1022 | -16382 | ^ E max | +127 | +1023 | +16383 | === x86 Extended Precision Format === The [[https://en.wikipedia.org/wiki/Extended_precision#x86_extended_precision_format | x86 Extended Precision Format]] is an exception insofar as it stores the non-fractional part of the mantissa //explicitly//. This has little practical impact; while FPUs up to the 80286 did use that integral mantissa bit for "unnormal" encodings, from the 80386 onward such "unnormal" values are no longer generated, and treated as invalid operand if encountered. The integral mantissa bit is zero for denormalized numbers and zero (obviously), and one for normalized numbers, infinity, and NaNs. A special case is the pattern for non-signalling NaNs (all exponent bits set, integral and highest fractional mantissa bit set, //with all other mantissa bits cleared//). This is considered a "floating point indefinite", a special case of non-signalling NaN. ^ Format ^ x86 Extended Precision ^ ^ Overall Width | 80 bits | ^ Significand | 63 (+1) bits | ^ Exponent | 15 bits | ^ Exponent Bias | 16383 | ^ E min | -16382 | ^ E max | +16383 | ====== Dragon 4 and Grisu 3 ====== The seminal works on the conversion of binary floating point to decimal string representation are: * Guy Steele, Jon White (1990): [[https://doi.org/10.1145/93548.93559 | How to Print Floating-Point Numbers Accurately]] * David Gay (1990): [[http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.4049 | Correctly Rounded Binary-Decimal and Decimal-Binary Conversions]] * Robert G. Burger, R. Kent Dybvig (1996): [[http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.52.2247 | Printing Floating-Point Numbers Quickly and Accurately]] * Florian Loitsch (2010): [[http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.1021.1906 | Printing Floating-Point Numbers Quickly and Accurately with Integers]] Steele & White reference another seminal work on the opposite conversion, from decimal string to binary floating point representation: * William D. Clinger (1990): [[https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4152 | How to read floating point numbers accurately]] That first paper by Steele & White presented the "Dragon" algorithm in its fourth iteration, to which Gay and Burger / Dybvig submitted improvements. Loitsch presented a significant performance improvement he called "Grisu", of which there are three iterations. (Thanks to [[https://www.ryanjuckett.com/programming/printing-floating-point-numbers/ | Ryan Juckett]] for writing a very nice introduction on the subject, which was at the root of what I assembled on this page.) ===== Dragon ===== Steele & White approach the presentation of their Dragon algorithm as a series of "lesser" algorithms building on each other. ==== Fixed Point Fraction Output ==== Given: * A positive value //f// less than 1, with //n// digits to (input) base //b// (usually 2). Output: * A value //F// of //N// digits to (output) base //B// (usually 10). Such that: - delim{|}{F - f}{|} < { b^{-n} } / 2 * The difference between representations is less than half the positional value of the //n//th digit of //f//. - N is the smallest number of digits so that 1. can be true. - delim{|}{F - f}{|} <= { B^{-N} } / 2 * The difference between representations is no more than half the positional value of the //N//th digit of //F//. - Each digit of //F// is output before the next is generated; no "back up" for corrections. Algorithm (FP)^3 (Finite-Precision Fixed-Point Fraction Printout): * k = 0, R = f, M = { b ^ { -n } / 2 } * while ( 1 ) * k++ * U = floor( R * B ) * R = ( R * B ) % 1 * M *= B * if ( R >= M AND <= 1 - M ) * append( F, U ) * else * break * if ( R <= 1/2 ) * append( F, U ) * if ( R >= 1/2 ) * append( F, U + 1 ) At the end, k is N, i.e. the number of digits in F. Example: * Given the base b = 2 number f = .10110, with n = 5, to be converted to base B = 10 * The //exact// decimal representation would be .6835 * The next higher number (f_{+1} = .10111) would be decimal 0.71475 * The next lower number (f_{-1} = .10101) would be decimal 0.65225 * The Mask would be M = { b ^ {-n} } / 2 = { 2 ^ { -5 } } / 2 = 0.015625 * First (k = 1) loop * multiply R = 0.6835 by B = 10 for integral part 6, fractional part 0.835 * multiply M = 0.015625 by B = 10 for new M = 0.15625 * Fractional part 0.835 is larger than mask 0.15625, and smaller than 1 - 0.15625 = 0.84375, so 6 is our first (fractional) digit, and the loop continues * Second (k = 2) loop * multiply R = 0.835 by B = 10 for integral part 8, fractional part 0.35 * multiply M = 0.15625 by B = 10 for new M = 1.5625 * Fractional part .35 is smaller than mask 1.5625, and not smaller than 1 - 1.5625 = -0.5625, so the loop terminates * Post-loop * Fractional part .35 is smaller than 1/2, so 8 is our next fractional digit * We have N = k = 2 fractional digits in our result of 0.68, which is the smallest N that uniquely identifies our original f = .10110 ==== Floating-Point Printout ==== Given: * A base //b// floating-point number v = f * b ^ (e - p), where //e//, //f//, and //p// are integers with p >= 0 and 0 <= f < b ^ p. Output: * An approximate representation to base //B//, using exponential notation if the value is very large or very small. Algorithm (Dragon2): * Compute v prime = v * B ^ {-x}, where x is chosen so that the result either is between 1 / //B// and 1, or between 1 and //B// (normalization, either with leading zero or leading non-zero digit) * Print the integer part of v prime * Print a decimal point * Take the fractional part of v prime, let n = p - floor ( log_ b v prime ) - 1 * Apply algorithm (FP)^3 to //f// and //n// * If x was not zero, append the letter "E" and a representation of the scale factor x