PostgreSQL's float4 and float8 Implementation in C
float4 and float8 are just an IEEE-754 float and double living inside a Datum — but PostgreSQL wraps every operation in overflow checks, declares NaN equal to itself and greater than everything, and lets a GUC change what the output function prints.
float4 (SQL real) and float8 (SQL double precision) are the two
IEEE-754 binary floating-point types. They are also the least mysterious
types in PostgreSQL: a float4 really is the four bytes of a C float, and
a float8 really is the eight bytes of a C double, stuffed directly into a
Datum. There is no varlena header, no TOAST, no palloc, no pointer.
That simplicity is exactly why the C is interesting. Because the raw bytes
are the value, the interesting decisions have nowhere to hide: they live in
the inline helpers in src/include/utils/float.h and the I/O functions in
src/backend/utils/adt/float.c. This is a walk through both, based on
reading the PostgreSQL source (version 20devel, commit e75fcc9867f).
The catalog entry
Everything about how a value is stored starts in src/include/catalog/pg_type.dat:
{ oid => '700', array_type_oid => '1021',
descr => 'single-precision floating point number, 4-byte storage',
typname => 'float4', typlen => '4', typbyval => 't', typcategory => 'N',
typinput => 'float4in', typoutput => 'float4out', typreceive => 'float4recv',
typsend => 'float4send', typalign => 'i' },
{ oid => '701', array_type_oid => '1022',
descr => 'double-precision floating point number, 8-byte storage',
typname => 'float8', typlen => '8', typbyval => 't', typcategory => 'N',
typispreferred => 't', typinput => 'float8in', typoutput => 'float8out',
typreceive => 'float8recv', typsend => 'float8send', typalign => 'd' },Read the fields as a contract:
| Field | float4 | float8 | Meaning |
|---|---|---|---|
typlen | 4 | 8 | fixed width, no length word |
typbyval | t | t | the value is the Datum |
typalign | i (4) | d (8) | alignment inside a tuple |
typstorage | p (default) | p (default) | not toastable |
typcategory | N | N | numeric-ish |
| array oid | 1021 | 1022 | _float4 / _float8 |
typstorage is not written, so it takes the catalog default of p —
TYPSTORAGE_PLAIN (src/include/catalog/pg_type.h), which is the only
sensible choice for a fixed-width by-value type. A float8 cannot be
compressed or moved out of line; it is 8 bytes, forever. Note also that
float8 carries typispreferred => 't' in category N (as does oid); that
flag matters when the parser has to pick a numeric type for an ambiguous
operator or literal.
The value is the Datum
Datum is uint64_t (src/include/postgres.h). Converting between a
float and a Datum cannot be a plain cast — C does not guarantee that
integer and floating-point values use the same registers or calling
convention — so postgres.h uses a union to reinterpret the bits:
static inline Datum
Float8GetDatum(float8 X)
{
union
{
float8 value;
int64 retval;
} myunion;
myunion.value = X;
return Int64GetDatum(myunion.retval);
}DatumGetFloat4 / Float4GetDatum do the same dance through int32. So
PG_RETURN_FLOAT8(x) is a bit-reinterpretation into a word, and
PG_GETARG_FLOAT8(0) is the inverse. Nothing is allocated.
Arithmetic is checked, not silent
If the value is just a C float, why not use a + b? Because C’s floating
point is quiet: it returns inf on overflow and 0 on underflow, and
PostgreSQL wants an error. The wrappers in src/include/utils/float.h add
the checks:
static inline float4
float4_pl(const float4 val1, const float4 val2)
{
float4 result;
result = val1 + val2;
if (unlikely(isinf(result)) && !isinf(val1) && !isinf(val2))
float_overflow_error();
return result;
}The test is deliberately specific: an infinite result is only an error if
neither input was already infinite. 'Infinity'::float8 + 1 is legal; the
overflow path is for finite inputs that escaped the range. Division is the
busiest case:
static inline float4
float4_div(const float4 val1, const float4 val2)
{
float4 result;
if (unlikely(val2 == 0.0f) && !isnan(val1))
float_zero_divide_error();
result = val1 / val2;
if (unlikely(isinf(result)) && !isinf(val1))
float_overflow_error();
if (unlikely(result == 0.0f) && val1 != 0.0f && !isinf(val2))
float_underflow_error();
return result;
}float_overflow_error() and friends are pg_noinline out-of-line
ereport() calls in float.c; the header comment explains that inlining
them at every call site would bloat the binary. The _safe variants
(float8_pl_safe, float8_div_safe, …) thread an escontext through so
the same logic can return a soft error for ErrorSaveContext.
There is even a build-time guard at the top of float.c:
#ifdef __FAST_MATH__
#error -ffast-math is known to break this code
#endif-ffast-math implies -ffinite-math-only, which would make the isinf()
checks above unreliable — and PostgreSQL’s float semantics depend on them.
NaN is a first-class value, with an opinion
IEEE-754 says every comparison with NaN is false, and each NaN is unequal to
even itself. That is a disaster for ORDER BY, B-tree indexes, and merge
joins, which need a total order. PostgreSQL therefore defines its own
comparison functions, all in float.h:
static inline bool
float4_eq(const float4 val1, const float4 val2)
{
return isnan(val1) ? isnan(val2) : !isnan(val2) && val1 == val2;
}
static inline bool
float4_lt(const float4 val1, const float4 val2)
{
return !isnan(val1) && (isnan(val2) || val1 < val2);
}The rule, spelled out in the header comment, is: all NaNs are equal to each
other, and every NaN is larger than every non-NaN. So NaN = NaN is true,
NaN > 1 is true, and NaN < 1 is false. The B-tree comparator is built on
those predicates (src/backend/utils/adt/float.c):
int
float4_cmp_internal(float4 a, float4 b)
{
if (float4_gt(a, b))
return 1;
if (float4_lt(a, b))
return -1;
return 0;
}btfloat4cmp / btfloat8cmp are the SQL-visible comparators, and
btfloat4sortsupport / btfloat8sortsupport install a direct
DatumGetFloat4-based fast comparator so a sort avoids the fmgr call.
Input
float4in and float8in are thin wrappers over float4in_internal and
float8in_internal. The float4 path is not a copy of the float8 path with a
smaller type — it deliberately calls strtof, and the comment explains why:
The motivation for using strtof() is to avoid a double-rounding problem: for certain decimal inputs, if you round the input correctly to a double, and then round the double to a float, the result is incorrect…
The comment works through 7.038531e-26, which lands exactly on the midpoint
between two floats when routed through a double, and therefore rounds the
wrong way. Using strtof rounds the decimal to float directly.
Both functions handle NaN and the infinities themselves, because the C
standard does not require strtod to accept all the spellings consistently.
They also distinguish ERANGE on a genuine out-of-range value from ERANGE
on a denormalized result (which they accept). A non-empty string with
trailing junk is a syntax error.
Output depends on a GUC
Here is the genuinely surprising part. What float8out prints is not a
property of the value alone — it depends on the extra_float_digits setting
(src/backend/utils/adt/float.c):
char *
float8out_internal(double num)
{
char *ascii = (char *) palloc(32);
int ndig = DBL_DIG + extra_float_digits;
if (extra_float_digits > 0)
{
double_to_shortest_decimal_buf(num, ascii);
return ascii;
}
(void) pg_strfromd(ascii, 32, ndig, num);
return ascii;
}When extra_float_digits > 0 (the default is 1, per
src/backend/utils/misc/guc_parameters.dat), PostgreSQL uses a Ryu-based
shortest-round-trip algorithm, double_to_shortest_decimal_buf from
src/common/shortest_dec.c. Otherwise it falls back to a fixed precision of
DBL_DIG + extra_float_digits (15 plus the GUC) via pg_strfromd. float4out
mirrors this with FLT_DIG and float_to_shortest_decimal_buf.
Because the result depends on a GUC, these functions cannot be IMMUTABLE.
They are marked STABLE in src/include/catalog/pg_proc.dat:
{ oid => '215', descr => 'I/O',
proname => 'float8out', provolatile => 's', prorettype => 'cstring',
proargtypes => 'float8', prosrc => 'float8out' },That provolatile => 's' is, in fact, the subject of this tree’s tip commit
— “Mark GUC-dependent type output functions as STABLE”.
Binary I/O bypasses text entirely: float8send/float8recv use
pq_sendfloat8/pq_getmsgfloat8 from libpq/pqformat.c, which write and
read the raw IEEE bytes in network order.
The float aggregates use Youngs-Cramer
AVG(), VARIANCE() and STDDEV() over floats do not accumulate
sum(X) and sum(X^2). The comment above float8_accum in float.c
explains that the schoolbook formula (N*sum(X^2) - sum(X)^2) / N^2 subtracts
two huge, nearly equal numbers and loses most of its precision. Instead the
transition state is a 3-element float8 array [N, Sx, Sxx], where
Sx = sum(X) and Sxx = sum((X - Sx/N)^2), updated incrementally. The
parallel combine step generalizes cleanly:
N = N1 + N2
Sx = Sx1 + Sx2
Sxx = Sxx1 + Sxx2 + N1 * N2 * (Sx1/N1 - Sx2/N2)^2 / N;Note that N is stored as a float8 to avoid defining a bespoke transition
type; the comment concedes that accuracy is fine until N exceeds about
2^52.
Look at it yourself
The catalog and the comparison semantics are both directly observable:
SELECT typname, typlen, typbyval, typalign, typstorage, typispreferred
FROM pg_type WHERE typname IN ('float4', 'float8');
-- NaN equals itself and sorts last
SELECT 'NaN'::float8 = 'NaN'::float8, 'NaN'::float8 > 1e308;
-- the GUC changes the printed digits, not the stored value
SHOW extra_float_digits;
SET extra_float_digits = 3;
SELECT 0.1::float8; -- more digits, same 8 bytes
SELECT pg_column_size(0.1::float8), pg_column_size(0.1::float4);pg_column_size reports 8 and 4 (plus, in a real row, whatever alignment
padding the tuple layout imposes — float8 needs 8-byte alignment, float4
only 4). There is no \d+ trick to see here, because there is nothing
variable-length to inspect.
What it all means
float4/float8are by-value, fixed-width, and never toasted. The value is the raw IEEE bits reinterpreted into aDatumthrough a union.- Every arithmetic operator is a checked wrapper, not a bare
+. Overflow and underflow on finite inputs raise errors; existing infinities pass through. - NaN has a defined place in the total order: equal to itself and greater than everything. That is what lets B-trees and sorts treat floats as a normal ordered type.
- Output is GUC-dependent.
extra_float_digitsselects Ryu shortest-round-trip output, which is why the output functions areSTABLE. float4input usesstrtof, notstrtod, specifically to dodge a double-rounding bug.
Further reading
src/backend/utils/adt/float.c— I/O, operators, comparators, aggregates.src/include/utils/float.h— checked arithmetic and NaN-aware comparisons.src/include/postgres.h—DatumGetFloat4/Float8GetDatumunions.src/include/common/shortest_dec.h,src/common/shortest_dec.c— Ryu output.src/include/catalog/pg_type.dat— thefloat4/float8catalog entries.