efloat: correctly rounded arithmetic and math library
efloat<nlimbs> is Universal’s adaptive-precision binary float: a sign, an int64_t exponent
and a significand of any number of 32-bit limbs. This page is the design record of its
arithmetic and its math library: what each function computes and how, why it computes it that
way, what was tried and dropped, how the results are verified, how fast they are next to MPFR,
and what is still open. It is written for whoever works on the speed or the accuracy next.
The code carries the local detail: every header states its algorithm, the error bound of each unrounded evaluation and the issue that introduced it. This page ties those together, and keeps the history and the measurements, which the code does not.
- Headers:
include/sw/universal/number/efloat/(efloat_impl.hpp,efloat_limbs.hpp,math/*.hpp), and the limb kernels ininclude/sw/universal/internal/limbs/limb_kernels.hpp, namespacelimb_kernels(efloat_mpnbefore #1823) - Regression tests:
elastic/efloat/{api,arithmetic,conversion,math}/ - Accuracy against MPFR:
benchmark/accuracy/efloat/mpfr_check.cpp - Speed against MPFR:
benchmark/performance/arithmetic/efloat/mpfr/mpfr_gap.cpp - Speed against mpmath and gmpy2:
benchmark/performance/arithmetic/efloat/mpmath_gap.{cpp,py} - Tracking: Epic #1763 (beat MPFR at 128-8192 bits), #1782 (this page), #1783 (cbrt, hypot, fma, fmod and remainder made correctly rounded)
1. The contract
Section titled “1. The contract”Every function below is designed to be correctly rounded:
the result is the exact mathematical value rounded once to the result’s precision, in whichever
of the four IEEE rounding modes efloat_rounding_mode selects (nearest-even, toward zero,
toward +inf, toward -inf). What backs that is not a proof. The arithmetic, sqrt, hypot,
fma and the conversions round exact results, and fmod and remainder return them, so they
are correct by construction. The other functions
rest on two things that are measured, not proven: the error bound of each unrounded evaluation
(section 2) and a retry limit that has not been shown to suffice (section 2, item 3). What
is established is agreement with MPFR on every case checked, 32-8192 bits, in all four modes
(section 6). Signed zeros, infinities, NaN and the exception flags follow C and IEEE 754.
The result’s precision (#1757, PR #1758). A value constructed from a native int, float
or double carries no precision of its own. An operation takes the wider precision of the
operands that carry one; when none does, the result rounds at the default precision
(set_default_precision(), else 32 nlimbs bits) and carries none either. A math function’s
result has its argument’s precision (result_precision() in math/exponent.hpp). So
x * 3 / 7 with x at 640 bits runs at 640 bits, not at the type’s width.
Before #1758, efloat<100>(3) carried 3200 bits, and x op literal silently ran at 3200 bits
(#1714 measured exp at 2048 bits running 9x slower than the 600 it was asked for). Of the four
options weighed in #1757 (exact literals sized to their bits; an exact flag that does not vote;
documentation only; mixed-type operators), the chosen design combines the second and the
fourth.
2. How a function is correctly rounded
Section titled “2. How a function is correctly rounded”All of the math library uses one scheme, in math/exponent.hpp.
- An unrounded evaluation with a stated error bound. Each function has a
*_working(x, target)that evaluates attarget + exp_log_guard_bits(64) bits and returns a value within2^(scale - target - 64 + error_bits)of the exact result. Each function’serror_bitsis a named constant next to its code (table in section 4). - Ziv’s test.
can_round(y, precision, correct)readsy’s bits from just past the rounding position down to the error bound. If they are neither all 0 nor all 1, no rounding boundary lies within the error, andyrounds to the same value as the exact result in every mode. - Ziv’s strategy.
round_ziv(evaluate, precision, error_bits)evaluates, tests, and if the test fails, evaluates again at twice the target. What it is given is a finite approximation; the premise is about the exact result, which must differ from every rounding boundary, so that some precision separates the two and an unbounded loop would terminate. The boundaries are binary fractions. Every caller returns the arguments whose exact result is a binary fraction before reachinground_ziv(item 4):exp2at an integer,log2of a power of 2,powat an exact power, and so on. What remains is irrational (transcendental, or algebraic likeexp2(1/2) = sqrt(2)) or rational but not a binary fraction, so never a boundary. The loop is bounded, though: after eight retries, at 256 times the target, it rounds the last value whether or not the test passed. So the guarantee is correct rounding unless the exact result lies within about2^-(256 P)of a rounding boundary, relative to its size; past that, the result is the last evaluation rounded once, which is faithful but not proven correct. There is no proof that eight suffice for any function here. The evidence is that every MPFR check has agreed, and the heuristic of the table-maker’s-dilemma literature (Lefevre and Muller’s searches of the hardest cases for double precision): a P-bit argument’s result is expected to come no closer to a boundary than about2^-(2P + c), for a small c, which a few doublings already resolve. The retry count is not reported, so a run cannot tell whether it reached the limit. - Exact results are formed, not approximated. A boundary is a binary fraction, so only an
exact result can sit on one:
exp2(k),log10(10^k),pow(x, n)with a narrow enough result,sqrtof a square,acos(1) = +0,tgamma(n) = (n-1)!and so on are computed outright and rounded once (#1774). They keep the argument’s precision (round_result). - Leading term plus a signed correction. When the result is
L + cwithLexact and|c| < 2^-(P+4) |L|of known sign (sin of a tiny x, erf near +-1, exp of a tiny x),round_near(L, direction, P)roundsLmoved2^-(P+32)of itself towardc, as MPFR’smpfr_round_near_xdoes.
The retry rate is low because the guard is wide: about 2^-29 per call for sin/cos/tan and
2^-45 for exp/log (#1772); exp_log_guard_bits = 64 is larger than a first try needs.
The error bounds are measured, not proven. Each *_error_bits was set from evaluations at
P against evaluations at 2P + 128, in all four modes (the intermediate operations round in
the current mode), across 32-8192 bits, and then given slack. The measured worst cases sit at
least 17-18 bits inside the declared bounds (#1772, #1774, #1779, #1781).
Ziv hides a missing guard. A subtraction that cancels exactly leaves trailing zeros, which
can_round reads as “too close to call”, so it retries at twice the precision and the final
result is still right. A deleted cancellation guard therefore passes every comparison of
results; only a direct check of the *_working error contract catches it (#1779). Tests that
cover a new function must check the contract, not just the rounded results.
3. Arithmetic
Section titled “3. Arithmetic”Storage is 64-bit limbs, least significant first (efloat_limbs.hpp, #1820): the format of
the kernels in limb_kernels.hpp, so operands go to them as they are and the exact result is
rounded once into the destination (round_into). The limbs live in a buffer inside the value,
up to EFLOAT_INLINE_LIMBS (8) limbs, 512 bits; a longer value takes a heap block, and freed
blocks go to a cache of the thread’s, so c = a + b and c op= b allocate nothing in a steady
state at any precision.
| operation | algorithm | thresholds | PR |
|---|---|---|---|
+ - |
aligned sum in a stack frame, 64 bits past the precision plus a sticky bit; in-place +=; sums and differences of operands at the full precision, any precision of whole 32-bit limbs, as MPFR’s mpfr_add1sp and mpfr_sub1sp, without the frame, one to four limbs in fixed-size code; a + b written into the result without a copy of a |
#1758, #1766, #1767, #1820, #1826 | |
* |
64x64->128 schoolbook, dedicated squaring (x * x), subtractive Karatsuba, blocks for unbalanced operands; from 16 limbs a short product of the high half first, rounded from when Ziv’s test passes on it, the full product otherwise; one to three limbs at the full precision in fixed-size code (#1826) |
karatsuba_threshold = 24, karatsuba_sqr_threshold = 64, mulhigh_threshold = 64 limbs (16, 48, 32 without the assembly); mulhigh_min_limbs = 16; Toom-3 from toom3_threshold = 240 (toom3_sqr_threshold = 352); x86-64 assembly rows (#1816) |
#1767, #1807, #1814, #1816 |
/ |
schoolbook with the Moller-Granlund 3-by-2 reciprocal, Burnikel-Ziegler divide and conquer (GMP’s dcpi1_div_qr) above the threshold; from it an approximate quotient (div_appr) rounded from where Ziv’s test passes, the exact quotient and its remainder, the sticky bit, otherwise; below it the exact quotient carries the precision alone and its remainder gives the next bit; a one-limb divisor as an integer one; quotients of one or two limbs by one or two by the processor’s divq (#1826) |
dc_div_threshold = 32 limbs (16 without the assembly); divq for a one-limb divisor up to 3 numerator limbs, and for two-limb quotients by two-limb divisors |
#1741, #1769, #1811, #1816, #1826 |
/ int |
divisors of up to 64 bits: the packed significand by one limb, Moller-Granlund 2-by-1 reciprocal (divrem_1), rounded once |
#1766, #1811 | |
sqrt |
integer root by Zimmermann’s Karatsuba square root (GMP’s mpn_sqrtrem) without its remainder, an approximate quotient at the top (GMP’s divappr), rounded from where Ziv’s test passes on it; the exact root and remainder, as the sticky bit, otherwise (exact squares among them); at whole 64-bit limbs up to 256 bits the exact root of the precision’s own limbs, its remainder giving the next bit (#1826) |
dc_div_threshold = 32 limbs (16 without the assembly) |
#1770, #1810, #1816, #1826 |
cbrt |
integer cube root by the same recursion carried over to the cube (cbrtrem); the remainder, the sticky bit, formed only next to a rounding boundary (section 4.8) |
#1800 | |
to double/float |
round once to the format, subnormals included | #1778 | |
| from decimal text | convert at the target plus 64 guard bits, retry wider only when the guard bits read 0111...1 |
widths 8192, 32768 | #1740 |
History and rejected alternatives
-
Division was bit-serial until #1741 (one compare/subtract/shift per quotient bit, about 37 multiplies’ worth at 608 bits). Knuth’s Algorithm D replaced it, then #1769 moved it to 64-bit limbs and divide and conquer. Newton reciprocal division was rejected (#1769): at 8192 bits a multiply costs 4.87 us and DC division 8.4 us, about 1.7 multiplies, while Newton needs three or more multiplies even with short products. GMP itself only switches to it past about 1000 limbs.
-
sqrtwas Newton’s iteration from a one-bit seed with a fixed 7 steps, which capped it at about 97 digits (#1140, fixed in #1143). It then misrounded near halfway points and set Inexact on exact roots, because it rounded from 64 guard bits without knowing the remainder (73,500-case MPFR check on the old code: 409 wrong results and 1,151 wrong Inexact flags). A Newton reciprocal square root was built and rejected (#1770): written in efloat operations it was slower than the old code below 8192 bits (0.686 us against 0.346 at 128 bits, while the exactsqrtremtook 0.126). The lesson recorded there: estimate the overhead of efloat operations before building an algorithm out of them; small sizes need limb-level code. -
Before #1767 a multiply made about six allocations. 64-bit limbs and Karatsuba made
a*b2.7x faster at 512 bits and 5.8x at 8192. -
Storage stayed 32-bit and most significant first through #1767, which changed only the kernels; #1820 moved it (below).
-
Zero signs were wrong in
*,/and exact cancellation until #1773 (#1768). -
sqrt without its remainder (#1810). The kernel was level with GMP’s (
sqrtrem4.89 us againstmpn_sqrtrem’s 4.30 at 8192 bits), butsqrtcost twice MPFR’s: MPFR asks GMP for the root alone, which skips the top level’s remainder, and efloat formed it every time.limb_kernels::sqrt_approxstopsdc_sqrtrem’s top level after the root: the squareQ^2and the one correction it decides are skipped, and the top division isdiv_appr.div_appr(GMP’sdivappr) forms the high half of the quotient exactly and the low half on the divisor’s top limbs, without the product that corrects it. Dividing by a truncated divisor can only raise a quotient, by at most 2, so each level of the recursion adds 2:divappr_n_error, 6 for a 64-limb quotient.- The root is then the exact floor or above it by at most
sqrt_approx_error(n), 6 at 8192 bits, against at least 30 bits past the precision, since precisions are whole 32-bit limbs. Ziv’s test on the limbs decides; where it fails, the exactsqrtremand its remainder do. Exact squares always fail it, since their roots end in zeros, and stay exact with no Inexact flag. sqrt_limbsreads the operand’s limbs in place and rounds once (assign_rounded_limbs), andload_shiftedpacks then shifts once. Before, the argument and the root were copied through efloat values, about 0.11 us of 0.12 at 128 bits.- The root is sized from the precision it rounds at, whole 32-bit limbs: a
sqrt_torequest of 33 bits rounds at 64 and now gets 66 bits of root, where it used to get 64, without a guard bit.sqrtitself always rounds at a multiple of 32, so its results did not change. hypottakes its root the same way (#1803).
-
Division from an approximate quotient (#1811). As
sqrtdid,b/aformed its exact quotient and remainder every time, where MPFR rounds from a short quotient: at 8192 bitsmpfr_div(4.36 us) costs less than GMP’s exactmpn_tdiv_qr(6.1).divide_magnitudetakes the quotient fromdiv_apprfrom 16 limbs, two 32-bit limbs past the precision, and rounds from it where Ziv’s test passes; a quotient next to a boundary, and every exact one, takes the exact quotient and remainder.div_apprnow also serves quotients longer than the divisor: their blocks are exact but for the lowest.divide_by_limbdivides the packed significand by a divisor of up to 64 bits (32 before) withdivrem_1, sized from the divisor’s bit length, where it ran on 32-bit digits into a new vector and then shifted, rounded and normalized it.assign_rounded_limbssplits each 64-bit limb, shifted to the leading bit once, into its two 32-bit halves, where the 32-bit frame reader it replaced read each half through a lambda, twice when the leading bit needed a shift: 0.375 us ofa/7’s 0.784 at 8192 bits.*and/round through it now too.- The operands load as packed 64-bit words (
load_msb_first, shared withsqrt). - What is left:
div_apprcosts 5.3 us at 8192 bits, againstmpfr_div’s whole 4.36, which uses a Mulders short division (divhigh); and below 16 limbsmpfr_divhas dedicated one- and two-limb paths (mpfr_div_1,mpfr_div_2), where efloat’s division is 1.5-1.7x.
-
Short products (#1807).
limb_kernels::mulhigh_nforms every partial producta_i b_jwithi + j >= n - 1, and some below, each exactly, so its high partR = r[n - 1, 2n)satisfies0 <= a b - R B^(n-1) < n B^n: the missing partial products,s + 1of them belowB^(s+2)at eachs <= n - 2, sum to under(n - 1) B^n B / (B - 1). Below 32 limbs the basecase forms then (n + 1) / 2products on and above the diagonal; above, Mulders’ recursion (AAECC 2000) forms the top0.6 nlimbs’ product in full and the two cross terms by short products. Both thresholds and the split were measured. From 16 limbs it costs 0.63-0.75 of the full product, and 0.59-0.65 of a square from 32 limbs (0.8 between 16 and 31, where the squaring basecase already skips half its products).*pads each operand ofm >= 16limbs with a zero limb below, son = m + 1andRis right to64 m + 62 - bit_width(n)bits, about 55 past a product rounded to the operands’ length. Ziv’s test on the limbs (limb_kernels::can_round) then decides between rounding fromRand forming the full product. All-ones operands, whose exact product lies one unit above a representable value whileRlies below it, take the full product.limb_kernels::mul_truncatedgives the fixed-point enginesfloor(a b / B^drop)or one unit below it, padding the operands so that the short product’s error is under one unit of the last limb kept. The series’ powers and Horner products, exp’s squarings and the versine’s first product and doublings use it from 16 limbs; each such product now counts as two truncations in the engines’ error budgets (sections 4.1, 4.3, 4.2), which stay inside their stated bounds. Below 16 limbs they keep the exact floor, by the same code as before.- Karatsuba from 16 limbs instead of 24 was 3-5% faster at 20 to 129 limbs, and the squaring
basecase up to 48 limbs instead of 32 7% faster at 129, with the portable basecase. The
assembly basecase (#1816) moved them back: 24 and 64 limbs, and the short product’s
basecase to 64 limbs, the division’s divide-and-conquer to 32. Builds that compile the
assembly use those; builds without it keep 16, 48, 32 and 16. An
addmul_2basecase, two rows a pass, was measured and dropped: the compiler already schedules the one-row loop as well. - What remains of
a*b’s gap at 2048 and 8192 bits is the kernel’s:mul_nruns 1.4-1.5xmpn_mul_n, whose basecase is assembly; the division kernel is level with GMP’s (0.8-1.2x).
-
Toom-3 (#1814):
toom3_mulsplits the operands in thirds, evaluates them at 0, 1, -1, -2 and infinity by Bodrato’s chain, multiplies the five pairs onk = ceil(n / 3)limbs (the evaluations’ top limbs, 4 at most, added back by one-limb products, as GMP’s Toom codes do), and interpolates by Bodrato’s sequence in two’s complement of2k + 2limbs, a halving an arithmetic shift and a third Jebelean’s exact division.- Measured, it pays only from 256 limbs (16384 bits): a product 2-16% faster there (9% at
512 limbs, 16% at 1024), a square 4-10% from 384 limbs; at 224 limbs and below Karatsuba is
as fast or faster. The thresholds are set there. At the efloat level
a*bgains 2.5% at 32768 bits and 5% at 65536, and nothing changes up to 8192. - So Toom-3 does not close the gap at 8192 bits. At 128 limbs Karatsuba’s 27 leaves, 16-limb
schoolbook products, are 69% of the time and run 1.24x GMP’s assembly basecase, and its
combining passes the rest. An
adcx/adoxbasecase written with intrinsics was measured and dropped: g++ made it 1.5-1.7x slower and clang no faster, since neither schedules the two carry chains as GMP’s assembly does. Inline assembly does (#1816, next).
- Measured, it pays only from 256 limbs (16384 bits): a product 2-16% faster there (9% at
512 limbs, 16% at 1024), a square 4-10% from 384 limbs; at 224 limbs and below Karatsuba is
as fast or faster. The thresholds are set there. At the efloat level
-
x86-64 assembly (#1816):
mul_1,addmul_1andsubmul_1run a loop ofmulx(b inrdx, flags untouched),adoxchaining the high halves through OF andadcxadding the destination through CF, four limbs a pass;submul_1adds the complement of each product limb on a CF chain that starts set, sor - p = r + ~p + 1, and its borrow is the high limb plus one less the final carry.add_nandsub_nare anadc/sbbchain. The loops step a negative index withleaand leave onjrcxz, which touch no flag; a compare and branch would break both chains, which is why intrinsics could not do it. The lown mod 4limbs run through the portable loop first and pass their carry in.- Gating. GNU inline assembly, so gcc and clang on
__x86_64__(Linux, macOS, MinGW); the multiply kernels also need BMI2 and ADX, checked once bycpuidat start-up (limb_kernels::x86::has_mulx_adx), so no-marchflag is needed and an older processor takes the portable loops. A static initializer that multiplies before the check runs seesfalseand takes them too. MSVC, ARM, POWER and RISC-V compile only the portable loops, as does any build withUNIVERSAL_LIMB_KERNELS_NO_ASMdefined. The results are the same bits either way: the kernels are exact. - Measured (i7-12700K):
mul_basecase0.92-1.09x GMP’smpn_mul_nat 8 to 24 limbs (was 1.1-1.24x), 15-30% faster than the portable loop from 16 limbs;mul_nat 128 limbs 1.08xmpn_mul_n(was 1.4x). At the efloat level, 8192 bits:a*b3.51 to 2.60 us,b/a6.27 to 4.61,sqrt3.76 to 2.92, and every row 3-26% faster at 2048 and 8192 bits (a+bleast), against the same build withUNIVERSAL_LIMB_KERNELS_NO_ASM. - The thresholds were measured again with it (above); Toom-3’s did not move.
- Testing. ASan and UBSan do not see inside inline assembly, so the
limb_kernelstest compares every kernel with its portable loop (mul_1_portableand the rest) at lengths 1 to 300, everyn mod 4, all-ones and all-zero runs, all-ones multipliers and in-place operands. - License. This is efloat’s own code, written from the instruction set’s semantics
(Intel SDM:
mulx,adcx,adox), not from GMP’s assembly; seedocs/licensing/mpfr-gmp-lgpl.md.
- Gating. GNU inline assembly, so gcc and clang on
-
Storage (#1820). Until #1820 the significand was a
std::vector<uint32_t>, most significant limb first, and every operation packed its operands into 64-bit scratch, rounded back into 32-bit limbs and resized the vector: at 8192 bits 88% ofa+b’s time was outsideadd_n, and at 128 bits 0.024 ofa*b’s 0.027 us was outside the kernel.- The limbs are now the kernels’ own.
efloat_limbs<N>holds 64-bit limbs, least significant first, the leading bit at the top of the top limb and no zero limb below the last 1 bit; a precision of an odd number of 32-bit limbs leaves the low half of the bottom limb zero. The exponent is the weight of the top bit, as before, soscale()and every algorithm that reads it are unchanged. - The storage decision. Of the three options #1820 named – a fixed array sized from the
template parameter, an inline buffer with a heap fallback, a fixed-capacity sibling type –
the inline buffer with a heap fallback, since precision is elastic at run time and can
exceed
nlimbs, where a fixed array would have to fail or truncate. The inline size ismin(ceil(nlimbs / 2) + 2, EFLOAT_INLINE_LIMBS), the precision and two limbs of the mathlib’s working precisions, andEFLOAT_INLINE_LIMBSis 8. Measured onefloat<256>: caps of 2, 4, 8 and 16 limbs makesizeof56, 72, 104 and 168 bytes; in-place operations run within 2% of each other at every precision from 128 to 8192 bits, since the block cache makes a heap value nearly as cheap as an inline one, and a copy at 512 bits costs 5.8 ns with a heap block against 1.8 inline. 8 keeps every value up to 512 bits inline at 104 bytes. - Allocation. Heap blocks come in powers of 2, and a freed one goes to a cache of 8 a
thread (blocks up to 2^16 limbs), which the next value or scratch request of its size
takes; a reaper frees the cache when the thread ends. The scratch of
*,/,sqrtand the mathlib’s kernels past their stack buffers comes from the same cache.elastic/efloat/api/storage.cppcounts every allocation through a replacementoperator new: none at 128 bits from the first operation on, and none in a steady state at 128 to 8192 bits forc = a op b,c op= b,a / 7andsqrt. - For #1819 (constexpr efloat). The storage works in constant evaluation: the inline
buffer is zeroed there, and a value past it allocates with
newand frees withdeleteinside the evaluation, which C++20 allows of a transient allocation; a constant result must fit the inline limbs. The arithmetic followed in phases 1 and 2 (the entry below); a fixed-capacity sibling type over the same kernels remains open. - The API.
bits()andlimb_vector()return 32-bit limbs, most significant first, converted from the 64-bit ones (limb_vector()now by value), andsetlimb()writes one, so the tests and applications that use them are unchanged. The mathlib readslimb_data()andlimb_count(), andset_significand()sets the 64-bit limbs. - Sums at the full precision. Operands of one sign, both as long as the precision, add as
in MPFR’s
mpfr_add1sp: Y shifted once into the result’s limbs, its shifted-out bits the guard and sticky bits, X added, and a carry shifting the sum down one bit. Every other sum goes through the frame, whose rounding shift now runs throughlimb_kernels::lshift. - Results are bit-identical to before, flags and carried precision included: 1.2 million
old-vs-new results over
+ - * /, the integer operands,sqrt,cbrt,fma,hypot, the truncations, comparisons, conversions todoubleandfloat, the mathlib andto_string-parseround trips, at 32 to 8192 bits, in every mode;mpfr_check0 wrong. - One behaviour changed. A parsed literal is normalized, as every other value is. Before,
parseleft zero limbs below the last 1 bit, so a parsed1.5compared neither equal to, less than nor greater thanefloat(1.5), andbits()listed the zeros.
- The limbs are now the kernels’ own.
-
Small precisions and differences (#1826).
mpfr_bands(section 7) measured the primitives at 20 precisions from 64 to 16384 bits, the half-steps between the powers of 2 and precisions of an odd number of 32-bit limbs among them. At the four precisionsmpfr_gapmeasures, every operation was within 1.4x MPFR; between them a differenceb - aran 2.1-3.4x MPFR up to 1024 bits, and one to three limbs (64-160 bits) 1.6-2.9x fora+b,a*b,b/aandsqrt.- Differences. The full-length path of #1822 takes differences, as
mpfr_sub1spdoes: Y shifted once into the result, the 64 bits it shifts out kept as a limb and any below as a sticky bit, X less Y with a borrow out of that limb. A gap of two bits or more cancels at most one leading bit; a gap of one bit can cancel any number, but then the low limb holds all of Y and the difference is exact, so it is shifted up once (2 to 63 bits) or, past a limb, rounds throughround_into. The same path takes precisions of an odd number of 32-bit limbs, rounding at bit 32 of the bottom limb, and one to four limbs run in code whose loops have fixed bounds and unroll. - No copies in
a op b. The binary operators wrote a copy ofaand thena op= b; they now write the result straight into its own limbs (assign_sum,assign_product,assign_quotient), and the general frame and the general product and division are out of line, so their scratch frames are not set up for the short paths: the prologues were a fifth of a 64-bit sum. Whats = a + bstill costs overs.assign_sum(a, b, false)is the temporary and its move, about 2.5 ns at 64 bits; the sum itself is level withmpfr_add. - Short products. One to three limbs at the full precision: schoolbook on 128-bit partial products in fixed-size code, normalized by at most a bit, rounded from the product’s limbs.
- Division. A one-limb divisor divides as an integer divisor does, the quotient sized to
the precision alone where that saves a limb, and the remainder R giving the next bit
(
2 R >= d) and the sticky bit, asmpfr_div_1does. Below the divide-and-conquer threshold the exact quotient also carries the precision alone, its remainder giving the rest, when no bit of the dividend is lost to the sticky bit (where it is,R + deltafor an unknowndelta < 1can straddleB / 2). Quotients of one or two limbs by one or two divide by the processor’s 128-by-64-bitdivqin the x86-64 assembly build (divrem_1_short, andsb_div_qr_short, Knuth’s Algorithm D on 64-bit digits, its estimate corrected by the divisor’s second limb), where a reciprocal costs about as much as the division. - sqrt. At a precision of whole 64-bit limbs up to 256 bits, the exact root of the
precision’s own limbs and its remainder R:
sqrt(N) >= S + 1/2exactly whenR > S, sincesqrt(N) = S + 1/2has no integerN. The approximate root needs a limb past the precision for its guard bits; at an odd number of 32-bit limbs the two roots have one length and the approximate one, which skips the remainder, is faster (96 bits: 0.033 against 0.036 us). - Rejected, by measurement. Moller and Granlund’s division-free reciprocal (RECIPROCAL_WORD,
a table and three Newton steps): 8.1 ns latency against 3.3 for the 128-by-64-bit division
it was to replace, which libgcc’s
__udivti3reaches with onedivqwhen the quotient fits a limb. The quotient sized to the precision for integer divisors (a/7): 5-15% slower from 64 to 384 bits, where it saves no limb, sodivide_by_limbkeeps a guard limb. Knuth’s Algorithm D for divisors of three limbs or more:sqrt8-12% slower at 512 to 1024 bits, whose recursion divides by such limbs. The exact root at 96 bits (above). - Results are bit-identical to before, flags and carried precision included: 3.1 million
old-vs-new results over
+ - * /, the integer operands,sqrt,cbrt,fma,hypot, the mathlib and the conversions at 32 to 8192 bits, among them full-length sums and differences at every gap of 0 to64 m + 2bits and products, quotients and roots of one to four limbs with all-ones and near-tie operands, in every mode, and the same in theUNIVERSAL_LIMB_KERNELS_NO_ASMbuild;mpfr_check0 wrong.
- Differences. The full-length path of #1822 takes differences, as
-
Short quotients and roots, and the copies between operations (#1828). After #1826,
mpfr_bandsstill hadb/aandsqrtat 64 and 96 bits at 1.7-2.6x MPFR, the difference at 384 to 1024 bits at 1.6-2.0x, and sums and differences of one to four limbs at 1.5-1.9x.- Division of one and two limbs. Operands of at most M = 1 or 2 limbs at a precision of
64 M or 64 M - 32 bits divide in fixed-size code without scratch frames, as
mpfr_div_1andmpfr_div_2do: A and D as integers of M limbs, Q = floor(A 2^(64 M) / D) of M limbs and a top bit from onedivqor fromsb_div_qr_short‘s two steps, and the remainder R. A quotient of 64 M + 1 bits has its own guard bit and R the sticky bit; one of 64 M bits takes the guard bit from whether 2 R >= D. The rounding isstore_short, the short products’ tail of #1826, now shared by the products, quotients and roots, and inlined into each: out of line, as g++ first left it, a 64-bit product took 9 ns instead of 7. - Roots of one and two limbs. N of 2 M limbs, and the exact root and remainder from
sqrtrem2or the newlimb_kernels::sqrtrem4,dc_sqrtrem‘s one level for four limbs in registers (10.8 ns against 13.7). At 96 bits the root’s 128 bits carry 32 past the precision, andsqrt_approx4replaces the second division: s’ and r’ of the top two limbs fromsqrtrem2, and Q = (r’ B + n1) / (2 s’) in double precision from a reciprocal of the top root computed alongside it, within 2^15 units of the root (nine roundings at most; 6069 units the worst of 20 million radicands). It rounds from its top 96 bits where the 31 bits under the guard bit are more than 2^16 from 0 and from 2^31; otherwise, about once in 2^14, the exact root does.sqrt()takes the short root without copying its argument first, andsqrt_to, the mathlib’s root, no longer copies it at all: the copy only reserved the root’s limbs, which an empty value gets from the block cache for less. - Differences one bit apart. A difference X - Y/2 that cancels its leading bit, which the
top limbs decide whatever borrows from below, is formed as 2 X - Y, twice the difference: no
shift of Y before and none of the difference after, which cost an in-place
lshiftpass at 512 to 1024 bits.limb_kernels::twice_sub_nforms it in one pass where the processor has ADX, as X + (X + ~Y + 1): the inner sum’s carry onadcx(CF), the outer onadox(OF), and thenotthat forms ~Y leaves both flags alone. One to four limbs take a fixed loop. - Copies of short runs.
copy_limbsleft runs of up to 8 limbs to a plain loop, which g++ vectorized into 32-byte loads. Read straight after the limbs were stored one at a time, as every result’s are, such a load cannot be forwarded from the stores and waits for them to retire, and everyc = a op bpaid it in the move of its result into c: scalar copies madeb-aat 160 bits 1.25x MPFR from 1.38x,a+bthere 1.49x from 1.61x,b/aat 64 bits 0.93x from 1.04x, and the rest of the short cells 3-10% faster. clang’s defaulted copies and moves of efloat copied the state, the sign and the exponent as one 16-byte vector, with the same stall: they are written out member by member, the exponent first. - Smaller things. Sums of three limbs and more write the result’s own limbs, as the long
ones did, and every short sum copies all of its limbs and trims a zero bottom limb after,
where it searched for zero limbs first;
fma’s result no longer starts as a copy of its longest operand. - Rejected, by measurement. A move of the whole 64-byte inline buffer by
memcpy: the same store-forwarding stall,b/aat 64 bits 9.4 ns to 10.2. Branchless carries and cancellations for one to four limbs:mpfr_bandsrepeats its operands, so the branches predict, andb-aat 128 bits went from 1.63x to 1.80x,a+bat 256 bits from 1.47x to 1.69x. A fused shift and subtraction in C for five limbs and more: 1.58x at 512 bits against 1.29x fortwice_sub_n, and slower thansub_nat 2048. Short sums of one and two limbs written in place: their limbs leave the registers (b-aat 64 bits 1.52x to 1.60x). The reciprocal forsqrt_approx4from s’ rather than from the top limbs’ own root: the division then waits forsqrtrem2(1.52x to 1.57x). The general sum out of line, a newest-first search of the block cache, the reaper armed once a thread: no change. The heap paths ofefloat_limbsout of line: clang gained 5% at 128 bits, g++ lost 4% at 544. - Not reached.
b-aat 128 bits stays at 1.7x MPFR anda+bat 160 bits at 1.5x: what is left is the temporary ofs = b - aand its move, about 2 ns, against MPFR’s two-limb subtraction, which takes 5.8 ns in all. Under clang the short sums and differences stay at 1.3-2.1x andb/aandsqrtat 96 bits at 1.6x, where g++’s are 1.3-1.5x, from code generation rather than algorithm. - Results are bit-identical to before, flags and carried precision included: 45 million
old-vs-new results over
+ - * /, the integer operands,sqrt,cbrt,fma,hypot, the mathlib and the conversions at 32 to 8192 bits, among them short quotients and roots next to their rounding boundaries, squares a unit off, quotients next to exact ones and differences one bit apart cancelling a limb or more, in every mode, and the same in theUNIVERSAL_LIMB_KERNELS_NO_ASMbuild;mpfr_check0 wrong. Of 32 deliberate mutations of the new code the tests catch 29; one is equivalent (2 R = D needs more bits than the operands hold), and two move the window ofsqrt_approx4by its margin, whose boundlimb_kernels.cpptests directly.
- Division of one and two limbs. Operands of at most M = 1 or 2 limbs at a precision of
64 M or 64 M - 32 bits divide in fixed-size code without scratch frames, as
-
The binary operators’ temporary, and clang’s code for short operations (#1830). After #1828,
mpfr_bandsstill hadb-aat 128 bits at 1.68x MPFR under g++ and 2.08x under clang,a+bat 160 and 256 bits at 1.5x and 1.7-1.8x, and under clangb/aandsqrtat 96 bits at 1.6x anda/7from 384 bits up at 1.4-1.6x. Counted withperf(the loops run at about 6 instructions a cycle, so the cycles follow the instructions), g++’s sum of two limbs took as many cycles as MPFR’s, 27: the rest was the temporary ofs = b - aand its move, about 40 instructions under g++ and 60 under clang. The short sums kept their limbs in memory, behind a stack protector, because the cancellation path took the address of the result’s array; clang also leftassign_sum,operator-, the move ofefloat_limbsandsb_div_qr_shortout of line, formed borrows withsetb/or/and, and made the second correction of the 2-by-1 division a conditional move.- Short sums in registers.
add_short<M>, for one to four limbs, does whatadd_full_lengthdid for them, in code whose loops have fixed bounds and unroll whole (EFLOAT_UNROLLED_LOOP): the result is a local array whose address is never taken, so it stays in registers, as MPFR’smpfr_add1sp1tompfr_sub1sp3keep theirs, and is stored once, bystore_short. A difference that cancels a whole limb, which only a gap of one bit gives, goes toadd_frame, which forms it exactly and rounds it once. A gap under 64 bits shifts within the limbs in two steps,(y << 1) << (63 - d), which gives 0 rather than a shift by 64 when d = 0; longer gaps take the general shift.add_full_lengthkeeps five limbs and more, without its fixed-size branches. - Inlining. The four binary operators,
assign_sumandadd_magnitudesare always inlined, so the result is built in the caller’s frame and its move follows without a call;assign_sum’s special cases moved out of line, toassign_sum_special. A move ofefloat_limbsfrom an inline value that fits copies its limbs in a few instructions, inline; a heap source, or one larger than the buffer, takes the out of linemove_heap, which keeps the self-move check.sb_div_qr_shortandassign_rounded_shortare always inlined too. - Carries.
add_carryandsub_borrowuse__builtin_addclland__builtin_subcllwhere the compiler has them, which clang turns intoadcandsbbchains, and the portable comparisons elsewhere.store_short’s rounding carry runs through every limb without an early exit, unrolled. - clang’s 2-by-1 division.
udiv_qr_2by1selects the first correction from two values computed side by side,r0 = n0 - q dandr1 = r0 + d, behind an emptyasmthat keeps clang from folding them back into a dependent chain, and takes the second correction, which is rare, on a branch marked unlikely rather than in a conditional move. clang’sa/7at 8192 bits went from 2450 cycles to 1943, against g++’s 1814 (unchanged). - Fixed-index copies.
divide_shortandsqrt_shortcopied their operands into the fixed-size frames with loops over a run-time count, which became calls tomemcpy; they now copy each limb by its fixed index, with a zero where the operand is shorter. - Loop annotations.
EFLOAT_UNROLLED_LOOPisvectorize(disable) unroll(full)under clang andnovectorwithunroll 8under g++; g++ 13 ignores (and warns of) anunrollon a loop whose condition decrements its index, so the annotated loops count with a plain step. - Rejected, by measurement.
add_magnitudesout of line alone: marginal. A test for d = 0 in front of the shift: g++’sb-a1-2 cycles slower. The first correction of the 2-by-1 division as a mask: g++ 14% slower at 8192 bits. The select without the emptyasm: clang folds it back.optimize("no-tree-slp-vectorize")on the short paths: unneeded once their loops carry the annotations. - Not reached.
fmaat 96 bits stays at 1.44-1.46x andb-aat 128 bits at 1.44-1.45x.fmais the generalfma_limbs, product, sum andround_intopath, with no fixed-size code for short operands; the operator’s temporary still costs about 0.4 ns of the difference’s 9. - Results are bit-identical to before, flags and carried precision included: the old-vs-new
sweeps of #1826 and #1828 (
+ - * /, the integer operands,sqrt,cbrt,fma,hypot, the mathlib and the conversions at 32 to 8192 bits, full-length sums and differences at every gap, short quotients and roots next to their boundaries) and a new one of in-place short sums and differences, deep cancellations, one-limb divisors up to 8192 bits and moves between inline and heap values, in every mode, three seeds each, and the same in theUNIVERSAL_LIMB_KERNELS_NO_ASMbuild;mpfr_check0 wrong.limb_kernels.cpptestsudiv_qr_2by1andudiv_qrnndon N = q d + r built from random q, d and r, andstorage.cppmoves onto themselves, of moved-from values and across lengths. Of 22 deliberate mutations of the new code the tests catch 18, and the other four are equivalent: the fallback for a whole limb cancelled in the plain difference, which the 2 X - Y path leaves unreachable; the 2 X - Y test narrowed, which only moves differences to the plain path, also exact;>for>=in the first correction of the 2-by-1 division, which the second correction absorbs; and the inline move’s fit test, whose other path steals the same limbs.
- Short sums in registers.
-
A short fma, and the last of
b-a(#1832). After #1830,fmaat 96 bits was the closestmpfr_bandscell, at 1.46x MPFR, andfmahad the most cells above 1.2x: 1.13-1.36x from 128 to 768 bits. At 96 bitsperfput a third of its cycles infma_limbsitself, a quarter inround_into, and the rest inlimb_kernels::mul,load_windowandadd_n, each a loop over run-time counts on a scratch buffer, for a product of 4 limbs in a frame of 5.- The short path.
efloat_detail::fma_short<M>, for x, y and z of at most M = 1 to 6 limbs at 64 M or 64 M - 32 bits, and z overlapping the product:scale(z) - scale(x) - scale(y)from1 - 64 Mto 64, the band where z’s limbs lie inside the product’s or at most one limb above them. The product’s 2 M limbs and the sum then fit a frame of 2 M + 1 limbs whose last bit is the product’s, so the sum is exact in it and the proxy offma_limbsis never needed. Each step is a loop of fixed length that unrolls: the operands top-aligned in M limbs, the schoolbook product asmultiply_short’s, z shifted to its place, the sum or difference byadd_carryorsub_borrow(moved from efloat tolimb_kernelsfor it), the two’s complement negated when z was the larger, the frame shifted up by its leading zeros, and the rounding bystore_short, as the other short operations round. Outside the band, or past 6 limbs,fma_limbsruns as before.fmaat 64 to 256 bits went from 1.05-1.46x MPFR to 0.54-0.92x under g++, and to 0.46-0.66x under clang, at 384 bits from 1.36x to 1.16x (clang 0.87x). - Operands in place. The general path, and
hypot‘s, copied the operands’ limbs into the scratch buffer before using them. The copy was vectorized, and its 32-byte loads of limbs each value had just stored one at a time stalled store forwarding (#1828); the kernels now readlimb_data()directly.fmaat 512 and 544 bits from 1.27x to 1.18x. - The temporary past the inline limbs. At 544 bits a value holds 9 limbs, one more than
efloat_limbskeeps inline, so the temporary ofs = b - atakes a block from the thread’s cache and its move givess’s old one back, about 14 cycles of 64.take_blocknow checks the block given back last before it searches, andgive_blockconstructs the reaper once, behind a flag in the cache: each use of athread_localwith a constructor called its init function. 3-4 cycles of the 14. b-aat 128 bits stays at 1.43-1.45x.perfcountsassign_sumalone at 27 cycles, level withmpfr_sub’s 27; the operator form takes 33. The difference is the temporary: its construction, the move’s copy of its two limbs by a loop over their count, and its destruction, spread over some 40 instructions with no single one dominant. A move that copied every inline limb regardless of the count would read limbs the temporary never wrote, and a result built in place ofsneeds expression templates; neither was done.- Rejected, by measurement. The short path up to 8 limbs: at 512 bits 1.42x MPFR against
1.27x for the general path, whose
mulis the assembly kernel, while 6 limbs (384 bits) went from 1.39x to 1.23x. - Results are bit-identical to before, flags and carried precision included: a new old-vs-new
sweep of
fmaat 32 to 2048 bits weighted to 256 and below, with z at each edge of the band and a bit either side, cancellations and sign changes, short operands and all-ones significands, plus the sweeps of #1802 (fma,hypot) and of #1826 to #1830, in every mode, three seeds each, and the same in theUNIVERSAL_LIMB_KERNELS_NO_ASMbuild;mpfr_check0 wrong.exact_functions.cppchecks the short path at every precision from 32 to 448 bits against the exact reference, at both sides of each edge of the band, with operands shorter than the precision, carries into the frame’s top limb, cancellations exact and a unit either way, and azof more limbs than the result’s that carries no precision. Of 17 mutants of the new code (the band’s edges, the limb bound, each carry and borrow, the negation and its sign, the sign of an exact zero, both shifts, the sticky bits, the exponent, the bits below the window, and the cache’s LIFO slot), the tests catch all 17. The boundnz <= Msurvived at first: azofM + 1limbs reachesfma_limbsonly when it carries no precision, and no test made one; the last case was added for it.
- The short path.
-
Arithmetic in constant evaluation (#1819, phases 1 and 2). The integer and dyadic constructors,
+,-,*,/, the compound operators with efloat and integer operands, the comparisons andsqrtrun in a constant expression, sostatic_assert(efloat<4>(3) + efloat<4>(4) == efloat<4>(7))compiles, andsqrt(2)to 1024 bits can be computed by the compiler. The results are the run-time ones, bit for bit.- The kernels. Every
limb_kernelsfunction isinline constexpr. The dispatchers to the x86-64 assembly (mul_1,addmul_1,submul_1,add_n,sub_n,twice_sub_n), thedivqofudiv_qrnnd, the emptyasmthat keeps clang’s select inudiv_qr_2by1, and the__builtin_addcll/subcllcarries each teststd::is_constant_evaluated()first and take the portable code there;mul128does the same for MSVC’s_umul128, the product of a build without__int128. Thedoubleseeds have integer counterparts there:sqrtrem1runs Newton’s iteration from 2^32 - 1, which descends to the root;cbrtrem_1finds its 64-bit root by a binary search over the bits withcube_1; andsqrt_approx4declines, so that its caller’s exact root decides, as it does near a rounding boundary. The roots are exact before they round, so either path gives the same result. - The scratch.
scratch_limbspast its stack limbs takesnewanddeletein constant evaluation instead of the thread’s block cache, as the storage does. - The thread’s state. The rounding mode and the status flags are thread-locals, which a
constant expression can neither read nor write:
rounding_mode()returnsRoundToNearestthere andraise_flags()does nothing, and every read and write inefloat_impl.hppgoes through them. One trap: the compiler tries the initializer of aconstvariable of integral or enumeration type as a constant expression first, soconst RoundingMode mode = rounding_mode();becameRoundToNearestat run time too.enforce_rangehad that line, andnumeric_limits.cppfailed in the directed modes until theconstwent. The mode is now never held in aconstvariable. - No run-time cost.
std::is_constant_evaluated()is a constant in each context, so the branches fold:mpfr_bandscompiles to the same instructions as before under g++ and clang, function by function,.textthe same size to the byte, apart from instruction order and register choice. clang gives a function an inline hint only wheninlineis written, andconstexpralone does not count: withinlinereplaced byconstexpr, clang inlined less and.textshrank by 5.5% (sqrt_limbs6158 to 1796 bytes), so the kernels sayinline constexpr. - Verification.
elastic/efloat/api/constexpr.cppchecks the arithmetic instatic_assert, compares the 30 results of a chain of operations evaluated by the compiler with the same chain at run time at 32 to 2048 bits, on and past the inline limbs, and computessqrt(2)to 1024 bits at compile time.limb_kernels.cppdoes the same for the kernels: products and divisions past the Karatsuba and divide-and-conquer thresholds, square and cube roots, a perfect cube, and the carries out of an all-ones limb and of equal limbs. Both pass with__int128undefined, as on MSVC, and with the assembly off. Of 7 mutants of the constant-evaluation branches, the tests catch 6; the seventh stopssqrtrem1’s iteration one step early, which its correction loop absorbs.
- The kernels. Every
-
The math library in constant evaluation (#1819, phase 3). Every function of the library runs in a constant expression, with the results the run time gives, bit for bit:
constexpr efloat<34> e = exp(one)at 1024 bits,static_assert(cbrt(efloat<8>(27)) == 3).- The constants. ln 2, ln 10, pi, atan(1/2), 2 / sqrt(pi), log(2 pi) / 2, the log table and
the Stirling coefficients are cached per thread at the widest precision computed so far. A
constant evaluation can keep neither a thread-local nor a value past the call that makes it,
so there
constant_value()computes the constant at the request’s precision plus the cache’s 32 guard bits and rounds it once. That can differ in its last bits from a value rounded from a wider cache, as two run-time caches of different widths can; the functions round correctly by Ziv’s test, so their results do not. The fixed-point series take their scratch from a vector of their own there (scratch_space), andlog2_factorialsums its table entry. - The estimates. The functions size their work in
double: series terms, halvings, block lengths, the bits a subtraction cancels, the precision lgamma’s parts call for. Those came from<cmath>, which a constant expression cannot call. They come fromefloat/double_math.hppnow, the same code in both:floor,ceil,trunc,round,nearbyint,fabs,ilogband the power-of-2 scale by the fields, bit for bit<cmath>’s over 20 million arguments;log2,logandlog10bysw::math::constexpr_math’s artanh series, within 2^-52 max(1, |log2 x|) ofstd::log2;sin(pi f)by its Taylor series; andlgammaby Stirling’s series and the reflection, within 2^-40 of max(1, |lgamma x|).sqrtis the one function with two paths: the integer root of the significand in a constant evaluation, the instruction at run time. IEEE-754 rounds a square root correctly, so they are the same value, and the instruction is ~30x faster, which exp, sin and expm1 measured (their halving targets take a root on every call: 4-11% slower at 128 bits through the integer path). - The conversions. efloat from and to
floatanddoubleread and write the fields of the encoding (extractFields,sw::bit_cast) instead ofstd::fpclassify,std::signbitandstd::frexp, and both directions round through one routine,round_to_native<K, E_min, E_max>(), a 128-bit window over the significand.long doubletakes the same routine: x87’s 64-bit significand and IEEE binary128’s 113 bits by their encodings, IBM’s double-double as the double nearest the value plus the double nearest the rest. It used to sum doubles and scale bystd::ldexp, which rounded to nearest in every mode and twice below the normal range: of 1.2 million values in the four modes, 249889 came out wrong, and the double-double round trip failed 149 times in 60000. Now none do (x87 here, binary128 under QEMU for RISC-V, double-double for POWER). The float and double conversions are unchanged, results and flags, over 1.6 million values in the four modes. - The thread’s state. The flags go through
efloat_detail::raise_flags();cbrt’s mirrored mode for a negative root, andrint, take the mode throughrounding_mode(); and the -inf oflog(0)carries x’s precision in a constant evaluation as at run time, which it did not. - The step limits. At 512 bits every function fits clang’s default 1048576 steps but the two
gamma functions, whose Stirling coefficients (m^2 / 2 products at the working precision)
a constant evaluation recomputes every call: about 4 million steps at 64 bits. A program that
evaluates lgamma or tgamma at compile time raises the limit, as the test does
(
-fconstexpr-steps, MSVC/constexpr:steps); gcc’s default operation limit holds them. - No run-time cost. Every function at 128, 512 and 2048 bits runs within the noise of before, lgamma 3-5% faster (its double estimate no longer goes through libm), and 125656 results of 41 functions at 7 precisions and in the four modes, with their flags, are the same as before under g++ and clang.
- Verification.
elastic/efloat/math/constexpr_mathlib.cppevaluates each function at six arguments, each value a constant evaluation of its own, at 128 bits and at 512 in level 2 (the gamma functions at 64 and 128), compares every result with the run time, checks the exact cases and the conversions instatic_assert, and computes exp(1), pi = acos(-1) and sqrt(2) to 1024 bits.
- The constants. ln 2, ln 10, pi, atan(1/2), 2 / sqrt(pi), log(2 pi) / 2, the log table and
the Stirling coefficients are cached per thread at the widest precision computed so far. A
constant evaluation can keep neither a thread-local nor a value past the call that makes it,
so there
-
Literals in constant evaluation (#1819, phase 5).
parse()takes astd::string_viewand isconstexpr, and a constant evaluation reads a literal as the run time does, bit for bit;assign(),efloat_pi(),efloat_e(),efloat_phi(),efloat_pi_2()andefloat_pi_4()areconstexprwith it, and pi read from its literal at 1024 bits is, instatic_assert, the limbs ofacos(-1).- The converter.
parse()rounds the output of the shared decimal-to-binary converter (utility/decimal_to_binary.hpp), which isconstexprnow for every type that uses it. It computes oninteger<W>at the narrowest of 256 to 32768 bits that holds the literal. Its magnitude estimate for a short literal with a huge exponent calls<cmath>; a constant evaluation refuses such a literal, which efloat’sparse()refuses before reaching it anyway. Thestd::isspace,std::tolowerandstd::to_stringcalls are ASCII code of its own. - integer<> multiplication by size.
integer<W>’s multiply ran every limb pair of the width, whatever the operands held: the converter collecting 330 digits at 16384 bits did 37 products of 512 by 512 limbs. Past 16 limbs (512 bits) a row of a zero limb is skipped and a row ends at the multiplicand’s last nonzero limb, then ripples its carry. The products are the same; a parse of 1e-300 or of pi at 128 bits is 2.5-3x faster (of 0.1 unchanged), pi at 1024 bits 6.1 ms -> 0.11 ms, at 3300 bits 16.5 ms -> 0.31 ms, and a full-width product at 768 and 1024 bits 1.6-1.9x faster. Up to 16 limbs the fixed-trip loop stays, which the compilers unroll; at 17 limbs clang’s product is slower (60 -> 80 ns), gcc’s faster (96 -> 63 ns). - The step limits. pi, e or phi fit clang’s default 1048576 steps through 512 bits; 1024 and 2048 bits take about 4 million, the 3300 bits of the literal about 16 million clang steps and 64 million gcc operations. The test raises the limits for itself.
- Out of the constant evaluation.
to_double_expansion<N>(v)returns v as N doubles, each the conversion of what the ones before it left: hi and lo for add, four for aqd, built in the same constant expression. pi at 384 bits givesdd_piandqd_piexactly, the nearest expansions since #1849 correctedqd_pi’s fourth component. - No run-time change. 66176 parses (6016 literals at 11 precisions, two widths) and integer<> products at seven widths are the same as before under g++ and clang, as are the cfloat, posit, dd and qd parses of the same literals.
- Verification.
elastic/efloat/conversion/constexpr_parse.cppreads 24 literals, each a constant evaluation of its own, at 64 and 128 bits and at 512 in level 2, compares each with the run time, checks ties to even and the failures instatic_assert, and compares pi, e and phi with the run time at 128 and 1024 bits, and at 3300 in level 2.
- The converter.
-
constexpr variables at every precision (#1819, phase 4). An efloat whose limbs fit its inline buffer – up to
EFLOAT_INLINE_LIMBS64-bit limbs, 512 bits by default (#1820) – is itself a constexpr variable. Past it the limbs are on the heap, and a constexpr variable cannot keep what a constant evaluation allocated (C++20, and C++23 and C++26 too).efloat_constant<nlimbs>(efloat/efloat_constant.hpp) holds the value of anefloat<nlimbs>– state, sign, exponent, precision and limbs – in an array sized for nlimbs * 32 bits, and turns back into the same efloat:constexpr efloat_constant<32> pi = efloat_pi<32>(); // 1024 bitsstatic_assert(pi > 3 && pi < efloat<32>(22) / 7);efloat<32> x = pi; // bit for bit efloat_pi<32>()- Its interface. It compares with itself, with
efloat<nlimbs>and with native arithmetic types (a native operand converts exactly), converts to float and double, and has the selectors (iszero,isnan,sign,scale,get_precision,limb_data). Arithmetic goes through the efloat it converts to. A significand longer than nlimbs * 32 bits – computed at a precision set past the type’s – is rounded to nlimbs * 32 bits, asset_precision()rounds, to nearest in a constant evaluation. - Why a type of its own. The other designs #1819 weighed change efloat: a capacity from
the template parameter caps the elastic precision, and a capacity parameter of its own
changes every one of the 244 function templates over
efloat<nlimbs>. RaisingEFLOAT_INLINE_LIMBSstill works, for a whole build, at the price of larger copies.efloat_constantleaves efloat and its run time as they were. - NaN comparisons. efloat’s
>=was!(a < b), soNaN >= xwas true. It is nowb < a || a == b, false for a NaN as<=already was; for every other value it is the same. - Verification.
elastic/efloat/api/constexpr_constant.cppchecks inline constexpr variables at 128 and 512 bits, round trips of 10 values (fractions, a native integer without a precision of its own, both zeros, both infinities, both NaNs) at 32, 128, 544, 1024 and 3300 bits against the run time, the rounding of a longer significand againstset_precision(), the comparisons, and pi, e and phi asefloat_constant<32>, and<104>in level 2.
- Its interface. It compares with itself, with
4. The functions
Section titled “4. The functions”| function | algorithm | error bits | file |
|---|---|---|---|
exp |
x = k ln2 + r, r halved below 2^-T, T = 0.25 sqrt(F); series in fixed point by tapered rectangular splitting, squared back; abs(k) < 2^62 |
16 | exponent.hpp |
exp2, exp10, expm1 |
through exp; expm1 sums without the leading 1 and doubles back with u(u+2); exact 2^k, 10^k |
20 | exponent.hpp |
log |
below the switch (about 45000 bits): table of log(j/512) plus 2 atanh(v), abs(v) < 2^-10, the series in fixed point by rectangular splitting; above it: AGM, log s = pi s / (2 AGM(s, 4)) |
16 | logarithm.hpp |
log2, log10, log1p |
through log; log1p sums its own series for small abs(x) |
20 | logarithm.hpp |
sin, cos, tan |
reduce by pi/2 at widened precision; versine v = 1 - cos r from a halved argument, v / y^2 summed in fixed point by the series engine of exp, doubled back in scaled fixed point |
32 | trigonometry.hpp |
asin, acos, atan, atan2 |
one atan kernel: reflection, argument halved below 2^-T, T = 0.08 sqrt(W); atan(y) / y summed in fixed point by rectangular splitting |
24 | trigonometry.hpp |
sinh … atanh |
expm1 and log1p forms that do not cancel |
24 | hyperbolic.hpp |
pow(x, y) |
exp(y log x), log x widened by the magnitude of y log x; exact powers outright |
24 | pow.hpp |
pow(x, n) |
binary powering on 64-bit limbs, each product kept to its top limbs, bit_width(n) + 21 guard bits; one division for n < 0 |
bit_width(n) + 5 |
pow.hpp |
tgamma, lgamma |
Stirling series after shifting x up past 0.15 W, by a tapered Horner scheme; the shift’s factors multiplied in groups; reflection below 0; working precision predicted; exact (n-1)! |
22 | error_and_gamma.hpp |
erf, erfc |
positive-term series 2/sqrt(pi) e^{-x^2} sum t_n; asymptotic series for large x; 1 - erf with the cancelled bits added |
0 (32 guard bits) | error_and_gamma.hpp |
cbrt |
integer cube root floor(cbrt(m 2^(3F))) on 64-bit limbs (cbrtrem), the remainder formed only next to a rounding boundary; rounded once |
exact | sqrt.hpp, limb_kernels.hpp |
hypot |
squares, their sum and its integer square root on 64-bit limbs (sqrtrem), the remainder and dropped bits the sticky bit; rounded once |
exact | hypot.hpp, limb_kernels.hpp |
fma |
exact product and sum on 64-bit limbs in one frame (or a proxy for a negligible term), one rounding | exact | numerics.hpp, limb_kernels.hpp |
fmod, remainder |
exact integer remainder, 2^d mod y by binary powering |
exact | fractional.hpp |
“Error bits” is the function’s *_error_bits: its unrounded value lies within
2^(scale - target - 64 + error_bits) of the exact result.
4.1 exp family
Section titled “4.1 exp family”- Reduction.
k = round(x / ln2)from a double estimate, corrected once fromrwhenxpasses2^52;ln2at the working precision plus 64 bits, sok ln2keeps its accuracy for any|k| < 2^62(#1777). Before #1777 the range stopped at|x| < 2^17. Per-kwidths ofln2were rejected: they re-grew the cache and cost 9-12% at 8192 bits. - Series (#1788).
ris halvedhtimes, tos = |r| / 2^hbelow2^-T, andexp(+-s)is summed in fixed point (fixed_seriesinexponent.hpp).- Fixed point. Every value is a natural number
V = floor(v 2^F)inn + 164-bit limbs, withF = 64 nfraction bits andF >= P + 64 + T + 16. The binary point sits on a limb boundary, so a product drops whole limbs. Inside the series nothing normalizes, merges precisions or rounds; each step truncates. - Rectangular splitting (Smith 1989), with blocks of
m ~ sqrt(N)terms. WithT_k = A_i P_j / D_ijfork = i m + j, whereP_j = s^jandD_ij = (i m + 1) ... (i m + j), the series is a Horner scheme inP_m / D_imover the blocksB_i = sum_j P_j / D_ij. Each block is formed over its common denominator with one-limb multipliers (addmul_1) and one one-limb division. Only thempowers and theN / mHorner steps are full products.mis capped so that a block’s divisor fits in a limb. - Negative arguments. The terms alternate. Every partial value stays positive, so the kernel needs no signed arithmetic and no final reciprocal.
- Squaring back. The
hsquarings are fixed-point squares,n + 1limbs each.
- Fixed point. Every value is a natural number
- Error. There are about
N + 3N/m + mtruncations of2^-Feach, under2^10of them at 8192 bits. The tail of the series lies below2^-(F + 3), and thehsquarings multiply the relative error by2^h. That leaves2^(h + 11 - F) <= 2^-(P + 69), insideexp_error_bits. Measured against MPFR from 24 to 8192 bits, the worst case lies 33-73 bits inside the declared bound.elastic/efloat/math/exp_kernel.cppchecks the bound against a plain efloat Taylor series at2P + 256bits. 11 of 12 mutations of the kernel fail that test. The twelfth halves one time fewer, which changes only the speed, because the term count comes from the actuals. - Tapering (#1790). Block
benters the sum scaled byT_(bm) < 2^-G_b, so it is formed on the topn - q_blimbs, with64 q_b <= G_b - 8. Its truncation, scaled byT_(bm), stays below2^-(F + 8). On its way down a block, the Horner value gains the limbs the next block keeps. This tookexpfrom 1.1x to 0.8x MPFR at 2048 bits and from 1.6x to 1.1x at 8192. - The halving target.
T = 0.25 sqrt(F)is the optimum measured with tapering, to within the noise of a flat curve, from 128 to 8192 bits: 4 at 128 bits and 23 at 8192. Before tapering it was0.4 sqrt(F)(#1788). A cost model that priced a scalar step at a tenth of a product chose 34 at 128 bits and 46 at 8192, and ran 1.8x and 1.05x slower: it underrated what a squaring of a few limbs costs. - History. Before #1739:
ln2was a 59-digit literal (results stopped at about 2^-200) and the series stopped at 100 terms (about 680 bits). At 608 bitsexptook 957 us against mpmath’s 11 (#1714); #1741 brought that to 20 us and #1754 to 15.6 us. - History. Before #1788 the series ran in efloat arithmetic: one rounded multiply,
divide and add per term, after up to
sqrt(P)halvings. That was 2.3-3.3x MPFR. The fixed-point kernel (#1788) ran at 0.7-0.9x MPFR up to 512 bits, 1.1x at 2048 bits and 1.5-1.75x at 8192, over arguments with|r|from 0 toln2 / 2of either sign. Tapering (#1790) brought that to 0.7-0.8x up to 2048 bits and 1.1x at 8192. - Work per call (
|r| ~ 1/4, #1790 targets): at 128 bits,N = 34terms in blocks ofm = 6, 10 products and 3 squarings on 5 limbs. At 8192 bits, 281 terms withm = 7(capped so the divisor fits in a limb), 46 products, most of them tapered, and 22 squarings on 131 limbs. - Open. Binary splitting (MPFR’s
exp_3) for the widest precisions. At 8192 bits the products are most of the time, and efloat’s 8192-bit product is itself 2.2x MPFR’s, so Toom-3 would help there first. A two-limb block divisor would allowm ~ sqrt(N) = 14. The same kernel would also serve the series ofsin/cosandatanh, andexpm1below 1/2 still sums in efloat arithmetic. A cheaper first Ziv try is not needed for exp, which is already below MPFR’s time at small precisions.
4.2 log family
Section titled “4.2 log family”- Table and series (#1754, mpmath’s method below 2500 bits):
x = 2^e m,min[sqrt(1/2), sqrt(2)),c = j/512the nearest grid point, andlog x = e ln2 + log c + 2 atanh((m - c)/(m + c)), gaining about 21 bits a term. Keepingmaround 1 meansxnear 1 needs only the series, with no cancellation againste ln2. The 363 table entries are computed on first use and cached per thread. - The series in fixed point (#1795):
atanh(v) = v S(z),S(z) = 1 + z/3 + z^2/5 + ...,z = v^2(atanh_working_small), on atan’s engine (fixed_series::arctangent_series, #1792) without the alternation.Slies in[1, 1.1)forabs(v) < 1/2, sov Skeeps its relative precision asx -> 1. Block length0.7 sqrt(N), as for atan, measured again:0.5and1.0ran 3-7% slower at 8192 bits,1.420% slower. - Table entries (#1795) come from the same routine,
log(j/512) = 2 atanh(u/w)withabs(u/w) < 0.18: 7-10x cheaper than the efloat series they replaced (2048 bits: 16-55 us an entry, from 108-456; 8192 bits: 0.2-0.36 ms, from 1.4-3.3 ms). That matters now that the table runs at high precision, where each newjfills its entry once: overmpfr_check’s cold-cache argumentslogaverages 40 us at 2048 bits (main 83) and 1.4 ms at 8192 (main 2.3). - AGM (#1771; Brent 1976, Brent and Zimmermann Modern Computer Arithmetic 4.8.2, as MPFR
does it):
s = x 2^m > 2^(W/2),log s = pi s / (2 AGM(s, 4)),log x = log s - m ln2. About2 log2 Wsteps, each a multiply, an exact root and a halving. - The switch. AGM once
W > 4500 max(10, k)forxwithin2^-kof 1 (log_agm_bits_per_series_bit, measured: the series costsW M(W) / k, the AGMlog(W) M(W)). From the table the two cross near 45000 working bits; forx = 1 + 2^-knear5000 k(k = 20-40, at 65536 and 131072 bits). With the efloat series the constant was 144 (#1771) and the AGM ran from 1440 bits: at 8192 bits it takes 276 us, the fixed-point series 140, and at 2048 bits 34 against 9.6. - Error (#1795):
log_workinglies 14.6-40 bits insidelog_error_bits = 16at 24-8192 bits, measured against MPFR over randomx,xnext to 1, at and half-way between grid points and either side ofsqrt(2). Main’s series lay 14.1-40 bits inside, and the AGM it ran from 1440 bits up lay 65-70 inside, with its own guard bits. The minimum is the series’ final roundings at the working precision (the doc comment’s2^-(W - 3)), not the series.elastic/efloat/math/log_kernel.cppchecks the bound against four exact roots and the plain series at2P + 256bits. - History. Newton’s iteration on
exp(about 4.5expcalls perlog) until #1754 below 8192 bits and #1771 above. At 8192 bits that Newton had an absolute, not relative, error: forx = 1 - 2^-7481 rit was off by2^711ulps. #1771 madelog5x faster at 8192 bits (1441 to 289 us). - Open. The Sasaki-Kanada theta-function start (about a fifth fewer AGM steps). A second
table step to
abs(v) < 2^-20(#1795 left it out: the targets were met without it, and a 2^10-entry table would hold about 1 MB a thread at 8192 bits).ln2andln10still sum2 atanh(1/3)and2 atanh(1/9)term by term on first use at each precision.
4.3 Trigonometric
Section titled “4.3 Trigonometric”- Reduction (
reduce_half_pi):x = k pi/2 + r,|r| <= pi/4, withpicomputed by Machin’s formula at the needed width and cached. The subtraction cancels aboutscale(x) - scale(r)bits, andxcan lie as close tok pi/2as its own precision allows, so the reduction retries wider untilrkeepsbits + 64correct bits, up to three times2(P + scale(x)). This is a division at widened precision, not Payne-Hanek: its cost grows withscale(x).|x| < 0.785skips the division (#1766).- Below
2^40(#1790),k = round(x 2/pi)comes fromdouble. It lies within2^-12ofx 2/pi, sokis the nearest integer, or its neighbour next to a half-integer, which leaves|r|at most2^-12 pi/2pastpi/4; that is harmless to the versine.r = x - k pi/2is still formed, and widened, at full precision. This replaced the division at the working precision, a fifth ofsinat 128 bits.
- Below
- Tiny arguments (#1790).
sin,cosandtanreturnxor1, moved toward the next term, throughround_nearwhen that term lies below2^-(P+4)of the value. The cuts were 2 bits more conservative than the series allow. They are now2 scale(x) < -P - 3forsin(x^2/6 < 2^-(P+4)) and< -P - 4forcosandtan. At 128 bits that takessin(1e-20)from 0.51 to 0.125 us; MPFR takes 0.017.mpfr_checkdraws its trig special arguments on both sides of the cuts. Asincut loosened 5 bits past the bound fails it. - Versine kernel (#1760):
v = 1 - cos r, thencos r = 1 - vand|sin r| = sqrt(v (2 - v)), neither of which cancels. - In fixed point (#1790).
ris halvedhtimes, toy = |r| / 2^hbelow2^-T. Withz = y^2,v(y) = y^2 W(z) / 2, whereW(z) = 2 sum_k (-1)^k z^k / (2k + 2)! = 1 - z/12 + z^2/360 - ...lies in(0.9, 1]whatever the size ofy.- The series.
Wis summed on exp’s engine (fixed_series) with the divisorsd(t) = (2t + 1)(2t + 2), alternating and tapered. Fixed point loses nothing asr -> 0. - Forming
v.v = y^2 W / 2comes from azscaled to its own leading bit, asV = v 2^(F + E). - Doubling back.
v(2y) = 4v - 2v^2becomesV' = V - V^2 / 2^(F + E + 1)withEfalling by 2: one square and a subtraction per doubling.
- The series.
- Error of the fixed-point versine.
- A doubling multiplies the relative error by
(1 - v) / (1 - v / 2) < 1and adds one truncation, so the error does not grow withh. - With
F >= working + 24, the kernel is within2^-(working + 13). It returnsvrounded to the working precision (r’s precision plush), as the efloat kernel did. - Measured against MPFR from 24 to 8192 bits,
trig_workinglies 30.6-55 bits inside its declared bound forsin,cosandtan, and the versine within one ulp atr’s precision. elastic/efloat/math/trig_kernel.cppchecks both bounds against plain efloat Taylor series. 13 of 15 mutations of the versine and of the tapering fail it. Of the other two, turning tapering off changes only the speed, and dropping one limb too many in the taper is caught byexp_kernel.cpp.
- A doubling multiplies the relative error by
- The halving target.
T = 0.22 sqrt(F)was measured best, the optimum flat: 3 at 128 bits, 10 at 2048, 20 at 8192. Each halving gains the series inztwo bits a term, so the optimum lies below exp’s. - Work per call: at 128 bits, 19 terms in blocks of 5, 8 products and 2 doublings on 4
limbs. At 8192 bits, 155 terms in blocks of only 3, because the divisors are about
4 t^2, 54 products and 19 doublings. - Measured speed:
sin,cosandtanrun 0.9-1.1x MPFR up to 2048 bits and 1.1-1.2x at 8192 on the benchmark argument. Over arguments from1e-20to 100, inside and outsidepi/4and nearpi/2, they run 0.3-1.4x at 512 bits and up; the worst iscos(1.2), at 1.4x at 512 bits. At 128 bits they run 0.4-1.4x, with three exceptions:cos(1.2)at 2.1x;cos(-10.3)at 1.6x;- tiny
x, at 3.6-4.3x against MPFR’s 0.017 us fast path.
- History. The efloat kernel before #1790 (
h ~ 0.25 sqrt(P) + 6halvings,u ~ 0.21 P^0.35partial sums,(h, u) = (11, 2)at 512 bits,(17, 3)at 2048,(28, 5)at 8192; #1766) ran every term and doubling as a rounded efloat operation:sinat 2.7x / 2.2x / 1.9x / 1.5x MPFR at 128 / 512 / 2048 / 8192 bits. - Rejected (#1760): tapering the plain sin/cos series alone reached only 2.2x mpmath; halving with sin/cos double-angle formulas costs two multiplies a step and did not pay.
- atan (#1774): one kernel for
atan,asin,acos,atan2: reflect|x| > 1, halve bya / (sqrt(a^2 + 1) + 1), then the series. It replaced a series that gained 0.6 bits a term near|x| = 0.8(asin at 8192 bits: 13.8 ms to 0.5 ms). - atan in fixed point (#1792). With
ythe halved argument andz = y^2,atan(y) = y S(z),S(z) = 1 - z/3 + z^2/5 - ..., andSlies in(0.9, 1]: summed withF >= working + 24fraction bits it keeps its relative precision for anyy(fixed_series::arctangent_series). Its divisors2k + 1do not chain from term to term as exp’s do, so a block issum_j (-1)^j z^j / (2(i m + j) + 1)and the Horner step multiplies byz^malone. A block is summed in groups whose divisors’ product fits in a limb: one multiplication by a limb per term and one division per group. Tapering as in exp, withG_b = b m a. - Halving target (#1792):
ybelow2^-T,T = 0.08 sqrt(W)for the working precisionW, at least 2, measured best over a flat optimum:T = 2up to 512 bits, 4 at 2048, 7 at 8192. A halving costs a root and a quotient, four or five products, where a term costs one multiplication by a limb, so far fewer halvings pay than for exp (0.25 sqrt(F)) or the versine (0.22 sqrt(F)). The efloat series before #1792 halvedsqrt(W/10) + 1times, 29 at 8192 bits, and the halvings were 55% of its time. Block length0.7 sqrt(N):1.0 sqrt(N)ran 2-6% slower from 512 to 8192 bits,2 sqrt(N)30% slower at 8192. - Error (#1792):
atan_workinglies 21-46 bits inside its declaredinverse_trig_error_bits = 24at 24-8192 bits, measured against MPFR overatan,asin,acosandatan2; the kernel it replaced lay 19.5-27 bits inside.elastic/efloat/math/atan_kernel.cppchecks the bound against Euler’s series at3P + 256bits. - Open.
- Payne-Hanek for huge arguments.
- Binary splitting at high precision (MPFR’s
sin_cos). - Past
2^40the reduction still divides. - Odd quadrants form
sin rassqrt(v (2 - v)):cos(1.2)at 128 bits is 2.1x MPFR, the worst measured case. A sine series of its own in fixed point would remove the root. - A two-limb block divisor. The versine’s divisors are about
4 t^2, which caps its blocks at 3-5 terms. - mpmath caches a sin/cos table below 400 bits.
4.4 Hyperbolic
Section titled “4.4 Hyperbolic”sinh, cosh, tanh from expm1, as u(u+2)/(2(u+1)) and w/(w+2), so nothing cancels as
x -> 0; asinh, acosh, atanh as log1p of an argument that does not cancel
(atanh works on |x|; before #1774 atanh near -1 was off by up to 2^900 ulps).
4.5 pow
Section titled “4.5 pow”pow(x, y) = exp(y log x) with log x carried at the target plus the magnitude of y log x,
so its error is not multiplied by y (#1739). Exact powers are found first and rounded once:
x = 2^e with e y an integer; x^n whose n (bits(x) - 1) fits the precision; y = m/2^k
with |x| a perfect 2^k-th power (a mod-8 filter rejects most non-squares). pow(x, int)
uses binary powering with guard bits; before #1774 it was off by up to 32 ulps from repeated
rounded products.
Integer exponents on limbs (#1804). pow(x, n) and pow(x, y) with an integer |y| < 2^53
go first to integer_power_limbs, for |n| >= 2, x of at least two significant bits and
n (|scale(x)| + 1) < 2^60; everything else takes the path above.
- Binary powering from
n’s leading bit down, on the significand’s 64-bit limbs. Each square and product is kept to its topwlimbs, shifted to its leading bit (keep_top), so it is within2^-(64w - 1)of its exact value, relative to it, and its exponent is anint64_t. From 16 limbs each is a truncated short product ofw + 1limbs (#1807), which adds under2^-62of that bound;pow(b, 7)at 8192 bits went from 1.5x MPFR to 1.0x. - The error a step makes is doubled by every squaring after it, so
x^nis within2n 2^-(64w - 1)of exact:bit_width(n) + 1bits, and the test takes 4 more. efloat arithmetic carried 69 guard bits here before; the frame now carriesbit_width(n) + 21, andwis that plus the precision in limbs. n < 0: one division,floor(2^(64(na + w) - 1) / A),wlimbs and a top one, one more bit of error.- A power that kept every bit is exact and rounds from the limbs. Otherwise Ziv’s test runs on the
limbs (
limbs_can_round), and a failure doublesw, up to three times before the general path. A power that does not fit the frame has more thanP + 1bits, so it is on no rounding boundary, and some width passes.x = 1 + 2^-j + 2^-(P-2)withj > P/2 + 40makes the first width fail:x^n’s bits pastPare 0 up to about bit2j. - The result rounds once through
assign_rounded_limbs. No efloat is formed on the way, and none of the general path’s preamble runs (x == 1against temporaries, a copy of|x|,exact_pow,log_magnitude).
4.6 tgamma, lgamma (#1779, #1797)
Section titled “4.6 tgamma, lgamma (#1779, #1797)”- Stirling’s series (DLMF 5.11.1) after shifting
xup toz >= 0.15 W(gamma_shift_per_bit):lgamma x = lgamma(x + N) - log(x (x+1) ... (x+N-1)). The remainder is bounded by the first omitted term (DLMF 5.11(ii)). The series’ smallest term lies about9.06 zbits down, soz0 = 0.11 Walready reaches2^-W; 0.15 was measured by sweep (#1797): 0.3 spends shift factors where Stirling terms are now cheaper, and 0.12 adds terms. - The coefficients
B_2k / (2k(2k-1))come from the Brent-Harvey tangent-number recurrence (Modern Computer Arithmetic 4.7.2): positive additions and small-integer products, cached per thread. - The series is summed by Horner’s scheme in
1/z^2(stirling_sum, #1797), one product a term. Termkliesd_kbits below the first, so its partial sum is formed atW - d_k + 16bits, tapered as exp’s series is (#1754). - The shift’s product (
shift_product, #1797) multiplies its factors in groups of up to 8:(x + j0) ... (x + j0 + g - 1)is the polynomialsum e_i x^iwith natural coefficients below2^62, fromx^2 .. x^8computed once, so a group costsgproducts by a 64-bit integer and one full product. Groups shorten from about 2048 bits, where the coefficients reach2^62. - Below 0, reflection with
n = round(x)andf = x - n, which is exact. - The working precision is predicted before the first pass (
lgamma_precision, #1797):E, the scale of the largest part ((z - 1/2) log z, the log of the shift’s product, the log of the reflection’s sine), less the scale oflgamma x, both estimated in double. Near the zeros at 1 and 2 the estimate comes from|x - 1|and|x - 2|, which a double would lose. The shift alone cancels aboutlog2 z0bits, 11 at 8192 bits, more than the 8 spare bits #1779 started with, so every argument used to take a second pass at twice the width. A pass that still falls short, next to the zeros below 0 wherestd::lgammais no guide, is repeated with the shortfall added rather than at twice the width. tgamma = sign * exp(lgamma), with lgamma to absolute accuracy, atneeded + Ebits, also predicted (tgamma_precision);(n-1)!is exact while it fits twice the result’s width. The coarse lgamma pass that judges exp’s range runs only for|x| >= 2^50or tinyx.- Before #1779:
std::tgamma(double(x)), 53 bits at most, inf past 171.6.
4.7 erf, erfc (#1781)
Section titled “4.7 erf, erfc (#1781)”erf x = 2/sqrt(pi) e^{-x^2} sum t_n,t_n = t_{n-1} 2x^2/(2n+1)(A&S 7.1.6, MPFR’smpfr_erf_0): all terms positive, so nothing cancels;x^2exact; tapered precision.erfcfor largex: the asymptotic series (DLMF 7.12.1), used when its smallest term lies below the working precision (x^2 log2 e >= W + 8); otherwise1 - erf xwith the cancelled bits added (bounded by A&S 7.1.13); forx < 0,1 + erf |x|.- Results within
2^-(P+5)of +-1 (or of 2, forerfcof a negative x) round throughround_near.erfcreturns +0 with Underflow only past efloat’s exponent range. - Before:
std::erf(double(x));erfcwas 0 fromx = 27.2on, and all four modes returned the same value.
4.8 cbrt, hypot, fma, fmod, remainder (#1783, #1800, #1802)
Section titled “4.8 cbrt, hypot, fma, fmod, remainder (#1783, #1800, #1802)”Until #1783 these five were computed at the result’s precision but rounded more than once, and
mpfr_check found 889 to 2,063 wrong results each in 3,234 cases.
- Exact sums.
hypotandfmaadd two exact values:x^2 + y^2, or the exact productx y(exact_product:a + bbits for operands ofaandb) plusz.exact_suminexponent.hppadds them at the width that holds the sum exactly, except when the smaller lies wholly below the last bit2^Lof the larger carried atpadbits. Then it adds a proxy of the same sign,2^(L-2), instead. The true and the proxied sums lie strictly between the larger and its neighbour2^(L-1)away, so they round alike to any precision up topad - 2bits, and their square roots alike up to(pad - 4) / 2. That keeps the width bounded whatever the exponent gap.fmapassedpad = P + 2and rounded the sum once;hypotpassed2P + 4and took the exact root (sqrt_to, which rounds once). Since #1802 both work on limbs instead (next two items);exact_sumremains forfmaat the range’s ends,hypot’s far-apart arguments, andfmod. - fma on limbs (#1802). For finite nonzero operands with
|scale(x) + scale(y)| < 2^61, the significands’ productX Yis formed bylimb_kernels::mulstraight into a frame of 64-bit limbs, at the bit offset where its last bit lies, andz‘s limbs are added into it or subtracted from it in place. A borrow out of the frame’s top meanszwas the larger: the frame then holds the difference’s two’s complement, which is negated. The frame reaches from the lower of the two terms’ last bits to a carry bit above both, unless the term whose leading bit is lower lies wholly more than 66 bits below both the other’s last bit and 64 bits past the rounding point. That term is then a proxy of its sign at the frame’s last bit: the true and the proxied sums lie within two frame units of the larger term, which is a multiple of2^64of them, so they round alike.efloat::assign_rounded_limbsrounds the frame once, in the current mode. No efloat is formed on the way. Before, the two widened operands, the product, the two widened terms of the sum, and the sum were each an efloat with its own allocation: anfmaat 128 bits took the time of about four multiplications. - The short fma (#1832). Operands of at most 6 limbs whose
zoverlaps the product,scale(z) - scale(x) - scale(y)from1 - 64 Mto 64, takefma_short<M>: the same exact sum in a frame of2 M + 1limbs on the stack, in fixed-length loops, rounded bystore_short(section 3). - fma at the range’s ends (#1786). When
scale(x) + scale(y)leaves[min_scale, max_scale), the exact product would overflow or underflow, thoughx y + zmay not. Both terms are then scaled by2^-t, wheretis the larger term’s scale. A term more than2^62binades below the larger one is raised to2^(min_scale + 2). It still lies far below the larger term’s last bit, soexact_sumproxies it and the rounding is unchanged. The sum rounds atP, and multiplying by2^tbrings it back, overflowing or underflowing as a product would. For a finitex yand an infinitez,fmareturnsz; computingx * yfirst could overflow and giveinf - inf. - hypot scales both arguments by
2^-k,k = scale(max), so the squares stay inside the exponent range, and multiplies the root back. When the arguments are more thanmax(bits(max), P + 2) + 8binades apart, the result ismaxplus a positive amount below its last bit, and a proxy sum gives it directly. Otherwise (#1802), withF = 64 n - 1andnlimbs holdingP + 2bits and at least as many limbs asmax, the frameN = floor((x^2 + y^2) 2^(2F - 2k - 2c))takesX^2exactly andY^2floored;cis 1 when the sum is4^(k+1)or more, and halves the frame; the bits either floor drops are a sticky bit.Nis the only integer the exact sum floors to, andfloor(sqrt(N))is the exact root’s floor, since no square lies strictly betweenNandN + 1.sqrtremgives that rootSand its remainder, andSrounds once, with the remainder and the dropped bits as the sticky bit. - cbrt (#1800), the way
sqrtis (#1770).a = m 8^k,min[1, 8), andN = m 2^(3F)withF = 64 n - 1and at leastP + 18bits in the root.limb_kernels::cbrtremformsS = floor(cbrt(N))andR = N - S^3by Zimmermann’s square-root recursion carried over to the cube:- with
N = N' B^(3l) + A2 B^(2l) + A0,(S', R') = cbrtrem(N'),(Q, U) = divrem(R' B^l + A2, 3 S'^2), thenS = S' B^l + QandR = U B^(2l) + A0 - Q^2 (3 S' B^l + Q); Qdrops the last two terms of(S' B^l + Q)^3, so it is never too small and at most 4 units too large (measured: at most 2, and never with an odd number of limbs). Each unit is corrected byR += 3 S^2 - 3 S + 1,S -= 1, from the parts already formed;- a level costs a square of half the root, a division with a quotient of half the root, and
a product for the remainder; the base case is a double estimate, one Newton step in double
and an exact correction.
The top level skips the remainder.
S 2^-Fis then within2^-(F - 3)of the root and rounds as it is unless its bits from the guard bit on are all 0 or all 1 (can_round): at most2^-25of arguments (S carries 31 or 63 bits past a precision of whole 32-bit limbs), and every root of at mostP + 1bits. Then the exact root and its remainder, the sticky bit, decide. A shortmofbbits can be an exact cube only of a root of aboutb / 3bits; where that root needs a sixth of the limbs or fewer, its exact remainder decides first (cbrt(27)at 8192 bits). The result is correct in every mode with no Ziv retries; a negative argument rounds its magnitude in the mirrored mode. Before #1800 Newton’s iteration ran in efloat arithmetic with 80 guard bits under Ziv’s strategy, 2.5-3.3x MPFR’s time; before #1783 it ran at full precision from a one-bit seed and rounded unchecked: 6-33x MPFR’s time and wrong in a quarter of the cases.
- with
- fmod is exact: its result always fits the wider operand precision. With
2^lothe smaller of the operands’ last bits,X = |x| 2^-lo = mx 2^dandY = |y| 2^-loare integers, andX mod Y = (mx mod Y)(2^d mod Y) mod Y.2^d mod Ytakeslog2 dsquarings moduloY, so a quotient of2^(2^40)costs about 40 modular squarings, not2^40subtraction steps. Eachmodis an exact division, truncated and corrected by oneY. - remainder takes
|x| mod 2|y|: whether that is at least|y|is the quotient’s parity, which breaks the tie when the remainder is exactly|y| / 2. - Special values as IEEE 754:
fmod(x, inf) = x(before, NaN),fma(inf, 0, z)NaN, zero results signed asxforfmodandremainderand by the rounding mode forfma’s exact cancellation.
4.9 Constants
Section titled “4.9 Constants”The library computes its own constants at the working precision and caches the widest value
per thread: ln2 = 2 atanh(1/3), ln10 = 3 ln2 + 2 atanh(1/9), pi by Machin’s formula,
atan(1/2) = atan(1/3) + atan(1/7). The public efloat_pi/e/phi<nlimbs>() are 1000-digit
literals that carry the type’s width, constexpr since #1819’s phase 5; #1739 stopped using them inside functions, because a
value widened past the type’s width was reduced with a narrower pi. The first call at a new
precision pays to fill the caches, which matters for benchmarks.
5. Verification
Section titled “5. Verification”Four kinds of check, each catching what the others miss.
- Regression tests (
elastic/efloat/, run in CI): exact oracles where one exists (arithmetic/correct_rounding.cpp,conversion/subnormal_rounding.cpp,math/sqrt_precision.cppwith Hensel-lifted radicands that reach the sticky-decisive case), independent second methods (math/error_and_gamma.cppchecks erf against the alternating Maclaurin series carried with the cancelled bits), mpmath-generated vectors (tools/generators/efloat_exp_log_gen.py->verification/efloat_exp_log_vectors.hpp,efloat_parse_gen.py->efloat_parse_vectors.hpp), and the*_workingerror contracts. Each test added with a fix fails on the code before it (the PR states how many cases). - MPFR, bit for bit (
benchmark/accuracy/efloat/mpfr_check.cpp, built when MPFR is found): every function in all four modes at 32-8192 bits, five argument styles including each function’s special arguments. The ctest runs a quick sweep up to 512 bits; with no arguments it runs the full one in about 25 s. - Bit identity for speed work: a change that should not alter results is run over a
sweep of several hundred thousand results against the previous headers (
git show main:orgit archiveof every changed header into a scratch include tree) and must differ nowhere. - Mutation testing: each PR since #1767 broke its own fix in named ways (a guard set to 0, a sign flipped, a branch removed) and recorded which mutants the tests catch. Survivors are listed in the PRs; most are slack absorbed by another guard.
6. Measured accuracy
Section titled “6. Measured accuracy”mpfr_check, full run (scale 1, up to 8192 bits), MPFR 4.2.2, after #1783. On main at 2076edb7,
before it, the same run found cbrt 889 wrong, hypot 1656, fma 926, fmod 1546 and
remainder 2063, and 0 everywhere else.
Per function: 1000 cases at each of 32, 64 and 128 bits, 200 at 512, 30 at 2048 and 4 at
8192, a quarter in each rounding mode. A case is wrong unless value, sign and precision all
match MPFR. double and float are the conversions, half of them in or below the subnormal
range.
| function | cases | wrong | |
|---|---|---|---|
add |
3234 | 0 | |
sub |
3234 | 0 | |
mul |
3234 | 0 | |
div |
3234 | 0 | |
sqrt |
3234 | 0 | |
cbrt |
3234 | 0 | |
hypot |
3234 | 0 | |
fma |
3234 | 0 | |
fmod |
3234 | 0 | |
remainder |
3234 | 0 | |
exp |
3234 | 0 | |
exp2 |
3234 | 0 | |
exp10 |
3234 | 0 | |
expm1 |
3234 | 0 | |
log |
3234 | 0 | |
log2 |
3234 | 0 | |
log10 |
3234 | 0 | |
log1p |
3234 | 0 | |
sin |
3234 | 0 | |
cos |
3234 | 0 | |
tan |
3234 | 0 | |
asin |
3234 | 0 | |
acos |
3234 | 0 | |
atan |
3234 | 0 | |
atan2 |
3234 | 0 | |
sinh |
3234 | 0 | |
cosh |
3234 | 0 | |
tanh |
3234 | 0 | |
asinh |
3234 | 0 | |
acosh |
3234 | 0 | |
atanh |
3234 | 0 | |
pow |
3234 | 0 | |
powi |
3234 | 0 | |
tgamma |
3234 | 0 | |
lgamma |
3234 | 0 | |
erf |
3234 | 0 | |
erfc |
3183 | 0 | |
double |
3234 | 0 | |
float |
3234 | 0 |
The sweep takes about 25 s; mpfr_check 10 takes ten times the cases. The checks run for each
PR were larger and are recorded there: 42,520 cases for log (#1771), 53,728 for exp, sin, cos
and tan (#1772), 228,000 for the functions of #1774, 73,500 for sqrt (#1770), 321,600
conversions (#1778) and 19,338 each for gamma (#1779) and erf (#1781), all with 0 wrong after
the fix. 51 erfc cases are not compared: their results lie below MPFR’s smallest exponent, where
efloat returns +0 with Underflow.
7. Measured speed
Section titled “7. Measured speed”mpfr_gap, best of three, warm caches, microseconds per call, round to nearest.
Measured 2026-10-25 with #1832 (a fixed-size fma for short operands, section 3), pinned to one performance core (taskset -c 2), Intel Core i7-12700K, g++ 15.2 -O3 -march=native, MPFR 4.2.2, GMP 6.3.0;
a = 0.718..., b = 1.414..., x = 1 + a. Each cell is
efloat / MPFR in microseconds, then the ratio.
| operation | 128 bits | 512 bits | 2048 bits | 8192 bits | ||||
|---|---|---|---|---|---|---|---|---|
a+b |
0.011 / 0.013 | 0.8x | 0.021 / 0.020 | 1.1x | 0.033 / 0.049 | 0.7x | 0.088 / 0.170 | 0.5x |
a*b |
0.011 / 0.018 | 0.6x | 0.044 / 0.043 | 1.0x | 0.239 / 0.271 | 0.9x | 2.412 / 2.198 | 1.1x |
b/a |
0.015 / 0.029 | 0.5x | 0.090 / 0.080 | 1.1x | 0.485 / 0.492 | 1.0x | 4.239 / 4.381 | 1.0x |
a/7 |
0.021 / 0.025 | 0.8x | 0.046 / 0.040 | 1.1x | 0.110 / 0.106 | 1.0x | 0.382 / 0.382 | 1.0x |
exp(a) |
0.572 / 0.755 | 0.8x | 1.339 / 2.204 | 0.6x | 8.460 / 16.09 | 0.5x | 107.0 / 178.5 | 0.6x |
log(x) |
0.631 / 1.123 | 0.6x | 1.189 / 3.249 | 0.4x | 6.298 / 15.07 | 0.4x | 82.00 / 126.1 | 0.7x |
sin(a) |
0.604 / 0.783 | 0.8x | 1.570 / 2.149 | 0.7x | 9.617 / 14.42 | 0.7x | 125.8 / 197.3 | 0.6x |
sqrt(b) |
0.021 / 0.059 | 0.4x | 0.115 / 0.135 | 0.9x | 0.445 / 0.443 | 1.0x | 2.755 / 3.128 | 0.9x |
fma(a,b,a) |
0.020 / 0.029 | 0.7x | 0.059 / 0.050 | 1.2x | 0.379 / 0.334 | 1.1x | 3.348 / 3.200 | 1.0x |
hypot(a,b) |
0.082 / 0.175 | 0.5x | 0.199 / 0.328 | 0.6x | 1.010 / 1.034 | 1.0x | 8.064 / 8.210 | 1.0x |
cbrt(b) |
0.220 / 0.292 | 0.8x | 0.485 / 0.470 | 1.0x | 1.344 / 1.345 | 1.0x | 9.211 / 7.650 | 1.2x |
exp2(a) |
0.670 / 1.042 | 0.6x | 1.598 / 2.716 | 0.6x | 9.794 / 17.55 | 0.6x | 115.7 / 194.6 | 0.6x |
exp10(a) |
0.705 / 2.357 | 0.3x | 1.652 / 6.095 | 0.3x | 10.03 / 31.63 | 0.3x | 117.7 / 318.9 | 0.4x |
expm1(a) |
0.603 / 0.832 | 0.7x | 1.370 / 2.408 | 0.6x | 8.513 / 15.94 | 0.5x | 106.8 / 181.0 | 0.6x |
log2(x) |
0.711 / 1.292 | 0.6x | 1.329 / 3.514 | 0.4x | 6.924 / 15.62 | 0.4x | 86.56 / 131.3 | 0.7x |
log10(x) |
0.744 / 2.482 | 0.3x | 1.358 / 6.963 | 0.2x | 6.947 / 30.94 | 0.2x | 86.82 / 258.4 | 0.3x |
log1p(a) |
0.710 / 1.224 | 0.6x | 1.299 / 3.332 | 0.4x | 6.449 / 15.13 | 0.4x | 81.06 / 126.2 | 0.6x |
cos(a) |
0.532 / 0.536 | 1.0x | 1.349 / 1.723 | 0.8x | 8.799 / 13.20 | 0.7x | 121.3 / 191.6 | 0.6x |
tan(a) |
0.714 / 1.026 | 0.7x | 1.745 / 2.400 | 0.7x | 10.30 / 15.63 | 0.7x | 130.8 / 205.4 | 0.6x |
asin(a) |
1.474 / 2.889 | 0.5x | 3.824 / 11.08 | 0.3x | 19.02 / 44.53 | 0.4x | 200.7 / 347.3 | 0.6x |
acos(a) |
1.159 / 2.907 | 0.4x | 3.200 / 11.10 | 0.3x | 16.62 / 44.05 | 0.4x | 181.2 / 344.4 | 0.5x |
atan(a) |
1.060 / 2.396 | 0.4x | 2.779 / 9.028 | 0.3x | 16.14 / 42.67 | 0.4x | 178.2 / 339.1 | 0.5x |
atan2(a,b) |
1.007 / 2.402 | 0.4x | 2.960 / 11.06 | 0.3x | 15.95 / 45.46 | 0.4x | 175.7 / 330.2 | 0.5x |
sinh(a) |
0.699 / 0.885 | 0.8x | 1.568 / 2.493 | 0.6x | 9.205 / 16.35 | 0.6x | 111.8 / 186.3 | 0.6x |
cosh(a) |
0.687 / 0.879 | 0.8x | 1.552 / 2.503 | 0.6x | 9.181 / 16.36 | 0.6x | 111.7 / 184.8 | 0.6x |
tanh(a) |
0.712 / 0.976 | 0.7x | 1.618 / 2.562 | 0.6x | 9.449 / 16.82 | 0.6x | 113.5 / 188.2 | 0.6x |
asinh(a) |
0.905 / 1.381 | 0.7x | 1.783 / 3.682 | 0.5x | 7.821 / 15.80 | 0.5x | 88.13 / 131.0 | 0.7x |
acosh(b) |
0.947 / 1.406 | 0.7x | 1.875 / 3.737 | 0.5x | 7.216 / 15.84 | 0.5x | 81.34 / 130.9 | 0.6x |
atanh(a) |
0.811 / 1.407 | 0.6x | 1.536 / 3.563 | 0.4x | 7.507 / 15.68 | 0.5x | 90.90 / 131.3 | 0.7x |
pow(b,a) |
1.308 / 2.270 | 0.6x | 2.866 / 6.087 | 0.5x | 15.92 / 32.28 | 0.5x | 192.5 / 319.6 | 0.6x |
pow(b,7) |
0.065 / 0.106 | 0.6x | 0.166 / 0.193 | 0.9x | 1.036 / 1.280 | 0.8x | 8.457 / 11.21 | 0.8x |
tgamma(b) |
7.455 / 7.314 | 1.0x | 22.31 / 33.35 | 0.7x | 149.6 / 370.4 | 0.4x | 2487 / 7494 | 0.3x |
lgamma(b) |
7.091 / 7.615 | 0.9x | 20.91 / 34.22 | 0.6x | 140.1 / 772.3 | 0.2x | 2374 / 7377 | 0.3x |
erf(a) |
2.969 / 2.970 | 1.0x | 9.838 / 16.05 | 0.6x | 75.19 / 174.7 | 0.4x | 1411 / 3054 | 0.5x |
erfc(a) |
3.075 / 3.239 | 0.9x | 10.05 / 16.52 | 0.6x | 75.80 / 176.9 | 0.4x | 1418 / 3059 | 0.5x |
Epic #1763 asked for every ratio at or below gmpy2 (MPFR plus a Python call) and, as a stretch, at or below 1.5x MPFR; both held when it closed on 2026-10-29, re-measured then on main within 0.1 of these ratios.
Across precisions (mpfr_bands, #1826). The four precisions above are all whole numbers of
64-bit limbs, and the table times no subtraction. mpfr_bands times the primitives and three
functions at the powers of 2 from 64 to 16384 bits and the half-steps between them, on random
operands that fill their precision (a in [1/2, 1), b in [1, 2)), best of seven measurements
of 5 ms each; each cell here is the best of three such runs, efloat / MPFR before #1832 (at
#1831) and after it, under g++ (a+b at 64 bits before is the best of the last two: in the
first, MPFR’s timing ran slow and read 0.72x). Run it as benchmark_efloat_mpfr_bands all 1.5 to fail on a
ratio above 1.5.
| bits | a+b |
b-a |
a*b |
b/a |
a/7 |
sqrt |
fma |
exp |
log |
sin |
|---|---|---|---|---|---|---|---|---|---|---|
| 64 | 1.21 / 1.20 | 1.32 / 1.31 | 0.71 / 0.71 | 0.82 / 0.82 | 0.95 / 0.94 | 0.95 / 0.92 | 1.05 / 0.54 | 0.82 / 0.83 | 0.83 / 0.83 | 0.95 / 0.95 |
| 96 | 1.05 / 1.07 | 1.20 / 1.22 | 0.87 / 0.86 | 1.12 / 1.12 | 0.89 / 0.89 | 1.21 / 1.18 | 1.46 / 0.80 | 0.67 / 0.67 | 0.44 / 0.48 | 0.79 / 0.80 |
| 128 | 1.23 / 1.23 | 1.45 / 1.44 | 0.69 / 0.70 | 0.56 / 0.56 | 1.02 / 1.03 | 0.32 / 0.32 | 1.28 / 0.72 | 0.71 / 0.71 | 0.55 / 0.55 | 0.83 / 0.83 |
| 160 | 1.24 / 1.24 | 1.11 / 1.11 | 1.16 / 1.17 | 1.10 / 1.10 | 0.85 / 0.85 | 0.84 / 0.84 | 1.35 / 0.82 | 0.63 / 0.64 | 0.41 / 0.42 | 0.64 / 0.64 |
| 192 | 1.11 / 1.03 | 0.84 / 0.84 | 0.85 / 0.85 | 1.02 / 1.02 | 1.01 / 1.00 | 0.84 / 0.84 | 1.13 / 0.75 | 0.68 / 0.68 | 0.40 / 0.40 | 0.79 / 0.78 |
| 256 | 1.24 / 1.24 | 0.87 / 0.87 | 1.19 / 1.18 | 1.01 / 1.02 | 0.93 / 0.93 | 0.82 / 0.82 | 1.22 / 0.92 | 0.67 / 0.68 | 0.32 / 0.33 | 0.72 / 0.71 |
| 384 | 1.25 / 1.27 | 1.19 / 1.19 | 1.20 / 1.20 | 0.96 / 0.96 | 1.16 / 1.17 | 1.07 / 1.07 | 1.36 / 1.16 | 0.69 / 0.69 | 0.38 / 0.38 | 0.71 / 0.71 |
| 512 | 1.20 / 1.37 | 1.20 / 1.19 | 1.12 / 1.12 | 0.99 / 0.99 | 1.37 / 1.37 | 0.87 / 0.87 | 1.25 / 1.18 | 0.64 / 0.64 | 0.37 / 0.37 | 0.68 / 0.68 |
| 544 | 1.12 / 1.28 | 1.34 / 1.28 | 1.11 / 1.09 | 1.08 / 1.07 | 1.33 / 1.31 | 0.90 / 0.89 | 1.27 / 1.18 | 0.62 / 0.62 | 0.35 / 0.34 | 0.65 / 0.65 |
| 768 | 1.06 / 1.19 | 1.21 / 1.14 | 1.11 / 1.12 | 1.05 / 1.03 | 1.36 / 1.35 | 0.99 / 1.01 | 1.25 / 1.22 | 0.67 / 0.67 | 0.38 / 0.37 | 0.68 / 0.68 |
| 1024 | 1.22 / 1.16 | 1.09 / 1.03 | 0.88 / 0.87 | 1.03 / 1.03 | 1.20 / 1.18 | 0.96 / 0.97 | 1.00 / 0.99 | 0.56 / 0.56 | 0.34 / 0.34 | 0.69 / 0.69 |
| 1056 | 0.85 / 0.96 | 0.82 / 0.85 | 1.08 / 1.07 | 1.14 / 1.13 | 1.13 / 1.10 | 0.99 / 1.00 | 1.13 / 1.09 | 0.56 / 0.55 | 0.36 / 0.36 | 0.61 / 0.59 |
| 1536 | 1.06 / 0.99 | 0.71 / 0.74 | 0.85 / 0.84 | 1.12 / 1.10 | 1.07 / 1.08 | 1.00 / 1.00 | 0.95 / 0.93 | 0.50 / 0.50 | 0.42 / 0.42 | 0.65 / 0.65 |
| 2048 | 0.68 / 0.70 | 0.85 / 0.79 | 0.84 / 0.86 | 0.99 / 1.00 | 1.06 / 1.05 | 1.01 / 1.02 | 1.07 / 1.09 | 0.56 / 0.56 | 0.45 / 0.45 | 0.60 / 0.60 |
| 3072 | 0.57 / 0.65 | 0.86 / 0.88 | 0.99 / 0.99 | 0.98 / 0.97 | 1.06 / 1.06 | 1.04 / 1.04 | 1.05 / 1.04 | 0.51 / 0.50 | 0.53 / 0.53 | 0.61 / 0.62 |
| 4096 | 0.64 / 0.72 | 0.60 / 0.61 | 0.93 / 0.93 | 0.89 / 0.89 | 1.04 / 1.06 | 0.92 / 0.92 | 1.00 / 1.00 | 0.49 / 0.49 | 0.53 / 0.53 | 0.54 / 0.54 |
| 6144 | 0.50 / 0.55 | 0.65 / 0.65 | 1.04 / 1.04 | 0.88 / 0.89 | 1.04 / 1.04 | 0.91 / 0.92 | 1.16 / 1.17 | 0.51 / 0.51 | 0.63 / 0.63 | 0.62 / 0.62 |
| 8192 | 0.45 / 0.48 | 0.63 / 0.68 | 1.11 / 1.11 | 0.92 / 0.93 | 1.03 / 1.03 | 0.88 / 0.88 | 1.04 / 1.06 | 0.60 / 0.60 | 0.71 / 0.71 | 0.64 / 0.64 |
| 12288 | 0.51 / 0.54 | 0.68 / 0.68 | 1.07 / 1.05 | 1.01 / 1.02 | 1.07 / 1.07 | 0.93 / 0.92 | 1.23 / 1.21 | |||
| 16384 | 0.49 / 0.52 | 0.58 / 0.58 | 1.16 / 1.16 | 0.99 / 0.99 | 1.07 / 1.07 | 0.87 / 0.87 | 1.15 / 1.14 |
- Every cell is within 1.5x MPFR, under g++ and under clang.
fmaat 64 to 256 bits, which had the closest cell (1.46x at 96 bits), now runs 0.54-0.92x (clang 0.46-0.66x, from 1.07-1.44x), at 384 bits 1.16x (clang 0.87x, from 1.35x) and at 512 to 768 bits 1.18-1.22x (clang 1.12-1.21x, from 1.24-1.28x). The closest cell is nowb-aat 128 bits (1.44x, 1.43x under clang), where the operator’s temporary still costs about 0.4 ns of the difference’s 9 (section 3). From 1056 bits up every cell is within 1.25x, and the functions are at 0.33-0.95x throughout. - Under g++,
a+bat 512 to 768 bits reads 1.20/1.12/1.06x before and 1.37/1.28/1.19x after, and at 3072 and 4096 bits 0.57/0.64x and 0.65/0.72x, with code this change did not touch but for the block cache’s last-in-first-out check. In a translation unit of its own it is level or faster: 79.8 against 79.8 cycles at 512 bits, 87.2 against 82.9 at 544, 95.6 against 93.4 at 768, 160.0 against 155.8 at 3072 and 156.5 against 153.6 at 4096, with 11 instructions fewer per call from 3072 bits. Layout, as below; clang shows no such move.fmaat 4096 bits, whose operands the general path now reads in place, takes 3% more cycles in a translation unit of its own under g++ (5190 against 5350) and is level in the table (1.00x, clang 1.07x to 1.03x).
Against gmpy2 2.3.2 (mpmath_gap.py, the epic’s primary target, its figures including the
Python call) on the same machine, pinned to one performance core (taskset -c 2; unpinned runs
that land on an efficiency core read up to 3x slower), every cell of mpmath_gap’s table is now
below it. The closest are b/a at 2048 bits, 0.48 against 0.54 us, which #1820 was filed for
with the 0.549 it read before, and sqrt at 2048 bits, 0.53 against 0.56 (0.62 before #1820);
a+b at 8192 bits, the other cell #1820 named, is 0.07 against 0.22.
- Ahead or level with MPFR:
- the exp family (#1788, #1790) up to 2048 bits:
exp,exp2,expm1,sinh,coshandtanhat 0.6-0.9x,exp10at 0.3-0.5x up to 2048 bits and 0.7x at 8192,pow(b, a)at 0.9x up to 512 bits; sin,cosandtan(#1790) at 0.9-1.1x up to 2048 bits;erfanderfcfrom 512 bits up;atan,asin,acosandatan2(#1792) at 0.4-0.8x up to 2048 bits and 1.0-1.1x at 8192;- the log family (#1795) up to 2048 bits:
log,log2,log1p,asinh,acosh,atanhandpow(b, a)at 0.5-0.9x,log10at 0.3-0.4x.
- the exp family (#1788, #1790) up to 2048 bits:
- exp family (#1788, #1790): 1.1-1.2x at 8192 bits, against 2.3-3.3x before #1788.
erf,erfcgained 10-25% andtgamma1-7% through their exp calls. - sin, cos, tan (#1790): 1.1-1.2x at 8192 bits, against 2.1-3.3x at 128 bits and 1.5x at 8192 before.
- atan, asin, acos, atan2 (#1792): 2-3x faster than before, which was 0.9-1.7x up to 2048
bits and 3.0-3.1x at 8192. Over arguments from
1e-5to1e6, andasin,acosnear +-1, they run 0.2-0.8x MPFR up to 2048 bits and 0.6-1.1x at 8192. Tinyxat 128 bits is the exception:atan(1e-20)rounds fromx(11x MPFR’s fast path; 14x before). - log family (#1795): 1.3-3.6x faster than before, which was 1.0-1.3x up to 512 bits and
2.2-2.5x at 2048 and 8192; now 1.1-1.2x at 8192 (
log100.6x,pow(b, a)1.1x). Over arguments from2e-250to4e300, next to 1, at both ends of the table and withlog1p,asinh,acosh,atanhacross their ranges, they run 0.1-1.0x MPFR up to 2048 bits and 0.2-1.3x at 8192. Two exceptions at 128 bits:log1p(1e-6)at 2.1x (its own series, 3.1x before) andlog(1 + 2^-200), which rounds tolog 1there, at 5.6x on both.lgammaandtgammagained 5-11% below 8192 bits through their log calls. - tgamma, lgamma (#1797): 2.6-29x faster, from 3.0-6.3x and 4.8-18.3x MPFR to 0.3-1.4x. The
second pass at twice the width that every argument took was most of the time (112 of 136 ms
at 8192 bits); the working precision is now predicted. The tapered Horner sum, the grouped
shift product and the shorter shift (
0.15 W) did the rest. Over1 +- 2^-k,2 +- 2^-k,1.3e-6,1.3e6,1.3e100,-3 + 2^-30,-2.71,1000and30.5they run 0.02-1.5x MPFR (were 0.04-18x); the one row above that,lgamma(2 - 2^-200)at 128 bits (2.4x, 2.3x before), islgamma 2 = 0after rounding, under a microsecond on both. - cbrt (#1800): 1.9-2.7x faster, from 2.5-3.3x MPFR to 1.0-1.7x. Over
1e-300to1e300,mnext to 1 and 8, negative arguments, exact cubes and their neighbours, it runs 0.8-1.8x MPFR (2.1x for an exact cube’s neighbour at 8192 bits, againsta*b’s 2.2x).cbrt(27)runs 0.01-0.3x from 512 bits; at 128 bits it is 2.4x, a call under a microsecond, where a shorter root to try first would cost every full-width argument as much. - fma, hypot (#1802):
fma1.1-2.9x faster, from 1.7-4.2x MPFR to 1.4-1.8x;hypot1.4-3.7x faster, from 2.0-2.5x to 0.6-1.6x.fmanow costs 1.0-1.6 multiplications of its operands, as MPFR’s does (1.2-1.6), so its ratio followsa*b’s; at 512 bits it is 1.3 against MPFR’s 1.2, and the 1.7x there is mostlya*b’s 1.5x. Results are bit-identical to #1785’s, flags and carried precision included, over 1.1 million old-vs-new results in every mode. (#1785 had made both exact: before it,fmarounded twice and was wrong in 29% of the cases.) - The primitives set the floor:
a*bandb/arun 1.0-1.2x MPFR since #1820 (1.4-1.8x before #1816),a/70.9-1.2x andsqrt0.8-1.0x, and every function built on them inherits that. - pow(x, n) (#1804):
pow(b, 7)1.1-5.1x faster, from 3.4/2.8/1.9/1.6x MPFR to 0.7/1.0/1.3/1.5x;pow(b, -7)from 2.6/2.2/2.2/2.1x to 0.5/0.8/1.6/1.9x. Results are bit-identical to before, flags and carried precision included, over 1.44 million old-vs-new results in every mode (nup to2^31, negativen, exact powers,xnext to 1, powers next to the range’s ends,xthat carries no precision). - Short products (#1807): every function at 8192 bits now runs at or below 1.0x MPFR,
against 1.0-1.2x before; exp, its relatives, the hyperbolic functions and
pow(b, a)at 0.8x. At 2048 bits the same functions gained 10-25%.a*bwent from 1.8x and 2.2x to 1.6x and 1.7x at 2048 and 8192 bits,pow(b, 7)from 1.3x and 1.5x to 1.1x and 1.0x. Below 1024 bits nothing changed: the products there stay exact, by the same code. Results are bit-identical to before, flags and carried precision included: 920,000 function results across every family touched and 516,000 products, in every mode. - sqrt (#1810): from 2.2/2.0/2.3/2.0x MPFR to 1.0/1.0/1.2/1.2x, by the root without its
remainder and a limb-level wrapper (section 3);
hypotfrom 1.4x and 1.5x to 1.2x and 1.3x at 2048 and 8192 bits,asin,acos,atanandatan2by 5-14% through their roots. Results are bit-identical to before: 920,000 function results across the families, 512,000 sqrt and hypot results, 240,000 fma and hypot results and 80,000 cube roots, in every mode.fma(a,b,a)at 512 bits reads 1.9x here against 1.6x before, with the same code: in a translation unit of its own it is level under g++ and clang (0.0795 against 0.0796 us), a layout move of the kind described below. - Division (#1811):
a/7from 1.8/1.8/2.2/2.2x MPFR to 1.1/1.3/1.4/1.4x,b/afrom 1.8/1.7/1.8/1.9x to 1.8/1.5/1.4/1.4x, anderf,erfc0.2x lower at every precision through their divisions (section 3). Results are bit-identical to before across every family touched: 460,000 function results, 1.2 million quotients, and the product, fma, hypot, root and power sweeps of #1803 to #1810, 1.3 million more, in every mode.a*bat 128 bits reads 1.7x in this table (1.5x before); in a translation unit of its own it is faster than before under g++ and clang (0.0278 against 0.0294 us, 0.0299 against 0.0397). - Storage (#1820): the operations ran on 64-bit copies of 32-bit, most-significant-first
limbs; on the kernels’ own limbs, without per-operation allocation (section 3),
a+btakes 0.40-0.64 of its time before,a*b0.65-0.91,b/a0.67-0.91,a/70.74-0.94,sqrt0.85-0.91,fma0.83-0.90,cbrt0.72-0.92, and the functions 0.75-1.0 (most at 128 and 512 bits, where the overhead was the largest share). Every operation is now within 1.4x MPFR. The cost outside the kernels, the operation less its kernel on the same operands, is at 128 bits 0.012 us fora+b, 0.014 fora*band 0.016 forb/a(0.024, 0.024 and 0.033 before), against MPFR’s 0.007-0.011; at 8192 bits 0.06 fora+band 0.08 fora*b(0.20 and 0.32 before). #1820 asked for under 0.01 and 0.05: the rest is the generic rounding and the operation’s dispatch, where MPFR has paths for one and two limbs. - Small precisions and differences (#1826): against #1824,
a+btakes 0.62-0.95 of its time at every precision ofmpfr_bands,b-a0.50-0.82,a*b0.39-0.78 up to 192 bits andsqrt0.70-0.97 at whole-limb precisions up to 256 bits; inmpfr_gap,a+bat 128 bits went from 1.4x MPFR to 1.0x anda*bfrom 1.1x to 0.7x.a/7reads 4-11% slower at 64 to 256 bits under g++, with the same code as before: it is level under clang (18.8 against 18.9 ns at 64 bits), a layout move of the kind described below. - Short quotients and roots (#1828): against #1827 in
mpfr_bands,b/aat 64 and 96 bits from 2.13/2.60x MPFR to 0.92/1.32x,sqrtfrom 1.72/2.54x to 0.91/1.46x,b-aat 384 to 1024 bits from 1.57-2.00x to 1.18-1.47x and at 64 to 160 bits from 1.71-1.88x to 1.29-1.69x,a+bat 128 to 256 bits from 1.60-1.86x to 1.33-1.54x,fmaat 96 bits from 1.59x to 1.45x, anda*bat 64 to 256 bits 6-17% faster; inmpfr_gap,b/aandsqrtat 128 bits 0.66 and 0.59 of their time before.b/aat 512 bits reads 4% slower inmpfr_gapand 0.5% in a translation unit of its own, under either compiler: layout, as below. - The operators’ temporary and clang’s short code (#1830): against #1829 in
mpfr_bands, under g++b-aat 64 to 256 bits from 1.00-1.68x MPFR to 0.85-1.45x,a+bat 96 to 256 bits from 1.21-1.53x to 1.04-1.25x,b/aandsqrtat 96 bits from 1.31x and 1.44x to 1.12x and 1.22x; under clang the cells of the second bullet above. Inmpfr_gapb/aat 128 bits takes 0.79 of its time before under g++ and 0.65 under clang, and clang’sa/70.80-0.96 and its functions 0.92-1.00.fmaat 512 bits reads 4% slower inmpfr_gapunder g++ and 2% faster inmpfr_bands: layout, as below. - A short fma (#1832): against #1831 in
mpfr_bands,fmaat 64 to 256 bits from 1.05-1.46x MPFR to 0.54-0.92x under g++ and from 1.07-1.44x to 0.46-0.66x under clang, at 384 bits from 1.36x to 1.16x (clang 1.35x to 0.87x), and at 512 to 768 bits 3-12% faster through operands read in place. Inmpfr_gap,fma(a,b,a)at 128 bits from 1.1x to 0.7x (clang 1.2x to 0.6x) and at 512 bits from 1.3x to 1.2x.a+bat 8192 bits andb/aat 512 read 4-5% slower under g++ inmpfr_gap, with code this change did not touch: layout, as below (a+bthere is 1% faster in a translation unit of its own). - Largest gaps: in
mpfr_bands,b-aat 128 bits (1.44x) anda+bat 512 bits under g++ (1.37x, layout); inmpfr_gap,fmaat 512 and 2048 bits (1.1-1.2x) andcbrtat 8192 bits (1.2x).
Benchmark hygiene. Single runs of the mpmath comparison vary by 15-50%; compare branches
with best-of-5 runs on an idle machine. Warm the caches first. The first-call cost of a new
precision (filling ln2, pi, the log table, the Bernoulli numbers) is real but separate.
A change to one function can move unrelated rows by a few percent through code layout, since
mpfr_gap is one translation unit: #1790 moved untouched rows by up to 4% at 128 bits under
g++, and neither an isolated translation unit nor clang showed the move. #1792 moved fma at
512 bits by +3.5% under g++ in an isolated translation unit too, and by -2.5% under clang; its
code was byte for byte the same size, 48 bytes further along. #1795 moved a*b at 128 bits by
+4% in mpfr_gap, and by nothing in a translation unit of its own under either compiler. Check a surprising
row both ways before reading it as a cost.
8. History
Section titled “8. History”| PR | merged | change |
|---|---|---|
| #1143 | 2026-07-18 | sqrt/cbrt Newton iteration count grows with precision (was capped at ~97 digits) |
| #1739 | 2026-10-18 | computed ln2, ln10, pi, atan(1/2); 64 guard bits; series to convergence (exp/log/trig/pow stopped at ~200 bits) |
| #1740 | 2026-10-18 | decimal parsing at the value’s precision (long literals were truncated or 0) |
| #1741 | 2026-10-18 | Knuth division; small-integer divisors; normalize fix |
| #1754 | 2026-10-19 | log table + series; tapered exp series |
| #1758 | 2026-10-19 | precision semantics of literals; + - / rounding fixes; guard bits in hyperbolic, sqrt, cbrt |
| #1760 | 2026-10-19 | versine kernel for sin/cos |
| #1764, #1765 | 2026-10-19 | mpmath_gap at 128-8192 bits; mpfr_gap |
| #1766 | 2026-10-20 | in-place +=, squaring, rectangular splitting, one-limb division |
| #1767 | 2026-10-20 | 64-bit limb kernels, Karatsuba |
| #1769 | 2026-10-20 | divide and conquer division |
| #1770 | 2026-10-20 | exact sqrt (sqrtrem) |
| #1771 | 2026-10-20 | log by AGM above the switch; first Ziv retry |
| #1772 | 2026-10-20 | Ziv for exp, sin, cos, tan; round_near |
| #1773 | 2026-10-20 | zero and infinity signs |
| #1774 | 2026-10-21 | Ziv for every remaining function; exact results; atan kernel |
| #1777 | 2026-10-21 | exp range to 2^(2^62) |
| #1778 | 2026-10-21 | correctly rounded conversion to float/double below the smallest normal |
| #1779 | 2026-10-21 | tgamma, lgamma at any precision (were through double) |
| #1781 | 2026-10-21 | erf, erfc at any precision (were through double) |
| #1785 | 2026-10-21 | cbrt, hypot, fma correctly rounded; fmod, remainder exact |
| #1787 | 2026-10-22 | std::numeric_limits<efloat>; arithmetic overflows and underflows at the math library’s range, scale in [1 - 2^62, 2^62 - 1] |
| #1789 | 2026-10-22 | exp series in fixed point by rectangular splitting: exp family 2.3-3.3x MPFR to 0.7-1.7x |
| #1791 | 2026-10-22 | sin/cos/tan versine on the fixed-point engine; tapered blocks (exp at 8192 bits 1.6x to 1.1x) |
| #1794 | 2026-10-22 | atan/asin/acos/atan2 series in fixed point, fewer halvings: 0.9-3.1x MPFR to 0.4-1.1x |
| #1796 | 2026-10-23 | log’s atanh series in fixed point, AGM from 45000 bits instead of 1440: log family 1.0-2.5x MPFR to 0.3-1.2x |
| #1799 | 2026-10-23 | lgamma, tgamma: working precision predicted, tapered Stirling sum, grouped shift product: 3.0-18.3x MPFR to 0.3-1.4x |
| #1801 | 2026-10-23 | cbrt by an integer cube root with remainder (cbrtrem), no Ziv retries: 2.5-3.3x MPFR to 1.0-1.7x |
| #1803 | 2026-10-23 | fma and hypot on limbs, product, sum and root in one frame, rounded once: fma 1.7-4.2x MPFR to 1.4-1.8x, hypot 2.0-2.5x to 0.6-1.6x |
| #1806 | 2026-10-23 | pow(x, n) for integer n by binary powering on limbs with bit_width(n) + 21 guard bits: pow(b, 7) 1.6-3.4x MPFR to 0.7-1.5x |
| #1808 | 2026-10-23 | short products (Mulders’ mulhigh_n) for a*b with Ziv’s test and a full-product fallback, and truncated short products in the fixed-point engines and pow(x, n): every function at 8192 bits from 1.0-1.2x MPFR to 0.5-1.0x, a*b at 8192 bits 2.2x to 1.7x |
| #1812 | 2026-10-23 | sqrt from the root without its remainder (sqrt_approx, div_appr), the exact one only where Ziv’s test fails; a limb-level wrapper: sqrt 2.0-2.3x MPFR to 1.0-1.2x |
| #1813 | 2026-10-23 | division from an approximate quotient (div_appr) with Ziv’s test; a/n on 64-bit limbs for divisors of up to 64 bits; rounding from 64-bit frames: a/7 1.8-2.2x MPFR to 1.1-1.4x, b/a at 2048 and 8192 bits 1.8-1.9x to 1.4x |
| #1815 | 2026-10-24 | Toom-3 multiplication and squaring (Bodrato’s sequence), from 240 and 352 limbs by measurement: products 2-16% faster from 16384 bits; nothing changes up to 8192 |
| #1818 | 2026-10-24 | x86-64 mulx/adcx/adox kernels in inline assembly, chosen at run time by cpuid, portable loops elsewhere; thresholds re-measured: a*b, b/a, sqrt at 8192 bits 3.46/6.02/3.66 us to 2.61/4.58/2.91, below gmpy2 |
| #1822 | 2026-10-24 | 64-bit least-significant-first storage in an inline buffer with a per-thread block cache, no per-operation allocation: a+b 0.40-0.63x, a*b 0.65-0.91x, b/a 0.67-0.91x of before; every operation within 1.5x MPFR, every mpmath_gap cell below gmpy2; parsed literals normalized |
| #1824 | 2026-10-24 | the limb kernels moved out of efloat: efloat/efloat_mpn.hpp, namespace efloat_mpn, to internal/limbs/limb_kernels.hpp, namespace limb_kernels; macros UNIVERSAL_LIMB_KERNELS_NO_ASM and _X86_64_ASM (EFLOAT_MPN_NO_ASM and the old header accepted, deprecated, for one release, then removed after v5.12.0); no code change |
| #1827 | 2026-10-25 | mpfr_bands, efloat / MPFR at 20 precisions from 64 to 16384 bits; differences at the full precision as mpfr_sub1sp, at any precision of whole 32-bit limbs, one to four limbs in fixed-size code; binary operators into the result without a copy; short products of one to three limbs; one-limb divisors and short exact quotients rounded from the remainder, divq for quotients of one or two limbs; exact small roots; ratio guards in mpmath_gap: b-a 2.1-3.4x MPFR to 0.9-2.0x, a*b at 64-160 bits 1.6-2.3x to 0.8-1.5x |
| #1829 | 2026-10-25 | quotients and roots of one and two limbs in fixed-size code (sqrtrem4, sqrt_approx4), differences one bit apart as 2 X - Y in one pass (twice_sub_n), short copies without store-forwarding stalls, copies and moves member by member: b/a and sqrt at 64-96 bits 1.7-2.6x MPFR to 0.9-1.5x, b-a at 384-1024 bits 1.6-2.0x to 1.2-1.5x; every mpfr_bands cell within 1.5x but b-a at 128 bits and a+b at 160 |
| #1831 | 2026-10-25 | sums and differences of one to four limbs in registers (add_short), the binary operators, assign_sum and the inline move always inlined, carries by __builtin_addcll/subcll, clang’s 2-by-1 division without its conditional move: b-a at 128 bits 1.68x MPFR to 1.45x (clang 2.08x to 1.44x), clang’s b/a at 96 bits 1.63x to 1.03x and a/7 1.4-1.6x to 1.1-1.3x; every mpfr_bands cell within 1.5x under both compilers |
| #1833 | 2026-10-25 | a fixed-size fma for operands of one to six limbs that overlap the product (fma_short), operands of the general fma and hypot read in place, the block cache’s last-in-first-out slot and a reaper constructed once: fma at 64-256 bits 1.05-1.46x MPFR to 0.54-0.92x (clang 1.07-1.44x to 0.46-0.66x), mpfr_gap fma at 128 bits 1.1x to 0.7x |
| #1834 | 2026-10-26 | arithmetic in constant evaluation (#1819 phases 1-2): limb_kernels inline constexpr with the assembly, divq and carry builtins behind std::is_constant_evaluated(), integer seeds for the roots, new/delete scratch, the rounding mode and flags out of reach (rounding_mode(), raise_flags()): the constructors from integers, + - * /, the comparisons and sqrt in static_assert, bit for bit the run-time results; the same instructions at run time |
| #1847 | 2026-10-27 | the math library in constant evaluation (#1819 phase 3): cached constants computed per request (constant_value()), series scratch of their own, the double estimates from efloat/double_math.hpp instead of <cmath>, float and double converted by their encodings through round_to_native(), long double by its encoding: every function in static_assert, bit for bit the run-time results; run-time results and speed unchanged; long double now rounds in the current mode |
| #1848 | 2026-10-27 | literals in constant evaluation (#1819 phase 5): parse() from a std::string_view and the shared decimal-to-binary converter constexpr, efloat_pi/e/phi() with them, to_double_expansion() for a dd or qd out of a constant evaluation; integer<> multiplication past 16 limbs by the operands’ sizes, which makes a long literal’s parse up to 57x faster; results unchanged |
| #1851 | 2026-10-27 | constexpr variables at every precision (#1819 phase 4): efloat_constant<nlimbs> holds an efloat value in fixed storage past the inline limbs and turns back into the same efloat; >= false for a NaN, as <= was |
9. Open work
Section titled “9. Open work”Library gaps. None open: the exact integer cube root with remainder that this section asked
for is limb_kernels::cbrtrem (#1800).
Speed (past Epic #1763, closed 2026-10-29): short
division (divhigh); the fixed-point series engine of exp and the versine (#1788, #1790)
carried over to expm1 (atan done in #1792, log’s atanh in #1795); MPFR’s bit-burst atan at 8192 bits; a cheap first Ziv try at p + log2(p) guard bits instead of 64; binary
splitting for exp and sin/cos;
binary-splitting constants; Sasaki-Kanada AGM start; binary splitting for lgamma’s shift product;
a tapered fill of the Bernoulli table (since #1797 the first gamma call at 8192 bits fills 1439
terms in 1.1 s, against 915 in 0.4 s at the old 0.3 W shift; the shorter shift saves 1.5 ms
a warm call, so it pays after about 460 calls in a thread);
Payne-Hanek reduction for huge trig arguments; and from mpfr_bands: b-a at 128 bits (1.44x
MPFR under both compilers, #1832), where the binary operator’s temporary still costs about 0.4 ns
of the difference’s 9, which only a result written in place would save; and fma at 512 to 768
bits (1.2x), past the short path’s six limbs, where a seven- and eight-limb version measured
slower than the general path (section 3).
Constant evaluation (#1819): the gamma functions within the default step limits, which a
fixed table of Stirling coefficients would bring; and pi, e and phi past 512 bits within them,
which the converter’s fixed-width integer<> copies and shifts keep from.
10. Working on it
Section titled “10. Working on it”For a speed change:
- Measure first:
mpfr_gap name1,name2before and after, best of several runs. - Prove the results did not move: a bit-identity sweep against the old headers.
- Run
mpfr_checkin full. - If the change touches a guard or an error bound, re-measure the
*_workingcontract at 32-8192 bits in all four modes, and keep the declared bound at least 16 bits clear.
For an accuracy change:
- Reproduce against MPFR (
mpfr_check 4 fname), or against an exact oracle. - Add a regression test that fails on the old code, including the
*_workingcontract if there is one, and check that each part of the fix, reverted alone, fails it. - Mark the commit
BEHAVIOUR-CHANGE:(see CLAUDE.md), since results move.