Experiment: full-digit gamma in O(prec^1.5 log prec) by multipoint evaluation - #554
Draft
tompng wants to merge 11 commits into
Draft
Experiment: full-digit gamma in O(prec^1.5 log prec) by multipoint evaluation#554tompng wants to merge 11 commits into
tompng wants to merge 11 commits into
Conversation
Calculates `gamma(x)` by Lagrange interpolation of `b^x/x!` where `b` is `x.round`. Implements Binary Splitting Method version for small-digit number and Baby-Step Giant-Step version for full-digit number. Fallback to Stirling's asymptotic expansion if `x` is extremely large.
Lift the BSM [sum_num, mult_num, den] triple to polynomials in the batch offset z, build them with a product tree over fixed-point coefficients via Kronecker substitution onto Integer (GMP) multiplication, and evaluate at the arithmetic progression z = 0, m, 2m, ... Polynomial work is O(PREC^1.5 * polylog); evaluation is currently per-point Horner. Matches the BSGS implementation exactly up to 50000 digits in tests. Crossover is around 10000 digits (1.9x faster at 50000 digits). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the per-point Horner evaluation (the only PREC^2 term of the pipeline) with classical remainder-tree multipoint evaluation over the falling-factorial subproduct moduli: - power series inverses of the reversed moduli are memoized on the shared subproduct tree, - wide dividends are pre-reduced blockwise with R = t**count mod M_root, whose coefficients stay small, so no division wider than 2*count occurs, - Kronecker slot width now uses the two operands' separate maxima, which also speeds up small-by-large coefficient products elsewhere. Values agree exactly with the Horner path in all tests. Measured crossover against Horner is around m = 700 batches (roughly 250000 digits): below it Horner's machine-word constant wins, so eval_mode defaults to :auto. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pack folds negative coefficients borrow-style into the next slot, so one hex-join replaces the positive/negative double pack and giant subtraction. Unpack recovers signed slots by borrow propagation instead of adding a giant per-slot bias constant. Guard bits: measured loss with guard = 0 is 2.9 - 3.3 * m * n1.bit_length bits over prec = 300..10000, identical for both eval modes and for near-node x (the value dynamic range across batches dominates all other roundings). Set guard = 4 * m * n1.bit_length + 256, a ~20% margin, and record the measurement in the comment. gamma(sqrt2): 10000 digits 7.4s -> 6.1s, 50000 digits 104.9s -> 91.9s (horner mode); fast mode 123.9s -> 107.6s at 50000 digits. Results still agree exactly with the BSGS implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The batch leaf factors decompose as den_j = L_j * B_j * I_j and num_j = -b * L_(j-1) * G_j, where only L_j = (x - a0 - j) - z carries full-precision coefficients and B, I, G are small exact-Integer linears. The series numerator then takes the barycentric form F01 = sum_j (Omega / L_j) * w_j, Omega = prod L_i, with w_j collecting only small-coefficient factors. Tree nodes carry [Omega, Phi, BI, GX] with exact-Integer BI/GX side products, so the only wide-by-wide multiplications per merge are Phi_A * Omega_C and Omega_A * Phi_C against the degree-d Omega, instead of four products of degree-3d triples. The j = 0 term is attached at the root, where its Omega * BI part is F2 itself. gamma(sqrt2): 10000 digits 6.1s -> 4.8s, 50000 digits 91.9s -> 69.9s (2.96x over BSGS; crossover is now around 2000 digits). Results still agree exactly with the BSGS implementation in all tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gamma.gamma_lagrange now routes full-digit x to the multipoint pipeline when Integer multiplication is GMP-backed (Integer::GMP_VERSION) and prec is at least Multipoint.min_prec (3000, above the measured ~2000-digit crossover against BSGS). Multipoint.enabled = false is the kill switch; without GMP the pipeline stays off automatically because Toom-Cook multiplication would make it asymptotically worse than BSGS. This also removes the trap that reflected arguments (x < 0.5) fell back to the O(PREC^2) BSGS: reflection, lgamma and factorial doubling all reach the dispatch through gamma_lagrange. The test-only Multipoint.gamma wrapper is gone; gamma_mp_check.rb toggles Multipoint.enabled instead. BigMath.gamma(sqrt(2)/3): 8000/16000/32000 digits 4.3/16.8/65.7s -> 2.6/8.3/26.4s. BSM and factorial-doubling paths are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Represent the 2x2 batch transition product P_s(z) = prod [[den_t, 0], [num_t, num_t]] by the values of its entries at z = u * s instead of coefficients, and double via P_2s(z) = P_s(z) * P_s(z + s): the tables are extended by shift of evaluation values (Bostan-Gaudry-Schost) - one convolution with exact binomial weights, small-integer reciprocal kernel and exact incremental delta - then combined pointwise. No product tree and no separate evaluation step remain, so the total cost is a geometric sum over doublings: O(PREC^1.5 * log PREC), one log less than the coefficient engine. S = 2**kappa is even, which also removes the odd node count constraint of the coefficient engine. Measured loss with guard = 0 is 1.35 - 1.49 * S * n1.bit_length bits (prec 300..10000, near-node identical): the feared extrapolation amplification does not appear beyond the table dynamic range, so the guard is set to 2 * S * (bit_length + 4) + 256, smaller than the coefficient engine needs. gamma(sqrt2) 50000 digits: 49.1s vs 65.2s coefficient engine (4.2x over BSGS); crossover between engines is around 7000 digits, so engine defaults to :auto (:values from 8000 digits). Exact agreement with the coefficient engine and BSGS in all tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shift product prod (x - i) is a single-entry batch factorial, so the same value-table doubling applies with tables of degree s (a fifth of the main tables' work). The batch size is now chosen inside shift_prod_factor as a power of two, independent of the caller's batch count. This removes the last coefficient-domain component from the value engine: gamma(sqrt2, 50000) 49.2s -> 45.0s, and the whole value pipeline is now uniformly O(PREC^1.5 * log PREC). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The value-domain engine now covers the whole multipoint range: its crossover against BSGS is ~2500 digits, inside the existing min_prec = 3000 dispatch threshold, so the coefficient engine's niche is gone. Remove the barycentric pair tree, the Horner and remainder-tree evaluation modes with their subproduct/inverse-series machinery, the coefficient-polynomial helpers and the engine/eval_mode switches: the file shrinks from ~650 to 337 lines with a single error model (loss = 1.4 * S * n1.bit_length, guard = 2 * S * (bit_length + 4) + 256). The whole pipeline is O(PREC^1.5 * log PREC). Accepting ~1.3x in the 3000-8000 digit band compared to the retired engine buys one engine, one guard law and one code path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generalize batch_value_tables to take the leaf values [den_t, num_t] from a block, making the doubling driver client-independent; the gamma leaf definition moves into gamma_lagrange. incgamma_mp_check.rb computes gamma(x) for full-digit x in [0.5, 3] via gamma(a) =~ r**a * e**-r * (1/a) * (1 + sum prod r/(a+i)), reusing the layer's primitives. The constant numerator degenerates the 2x2 matrix: M_s = r**s is an exact scalar and only two degree-s tables remain, so the doubling is ~3x lighter per term than the gamma client's. Measured: exact agreement with BigMath.gamma at 200..50000 digits, and 1.4x - 2.3x faster than the Lagrange multipoint gamma (0.77s vs 1.74s at 5000 digits, 32.5s vs 44.7s at 50000). Loss law 0.26 - 0.50 * S * bl, smaller than the gamma client's (positive terms, narrower tables). The loss measurement also caught a wrong term-count estimate: the Gaussian tail approximation (1+sqrt(2))*r undercounts by ~13% (the true Poisson tail exponent gamma-(1+gamma)ln(1+gamma) gives ~2.72*r), which had cost a fixed ~28% fraction of the precision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rounding of x before the integer test happens in Gamma.gamma/lgamma ahead of the dispatch, so the multipoint path never sees an integer x. Inside the path the batch denominator D_k is the same computed value in the sum and in prod, so the near-node cancellation is exact. Measured against BSGS at prec 3000..4100 with x within 1e-(prec-10) of a node the error stays below 0.5 ulp. Add a test that crosses the dispatch threshold with such an x. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BigMath.gamma/BigMath.lgammafor a full-digit argument (anxwith asmany significant digits as the requested precision) currently cost
O(prec² · log log prec) with the BSGS branch of the Lagrange interpolation.
This branch evaluates the same barycentric interpolation sum in
O(prec^1.5 · log prec) and dispatches to it automatically when the
precision is at least 3000 digits and Integer multiplication is GMP-backed.
Results agree with the BSGS branch to the last digit in every test.
Measured on Apple Silicon with GMP 6.3.0,
gamma(sqrt(2), prec):The measured local exponent between 10,000 and 50,000 digits drops from 2.03
(BSGS) to 1.53.
How it works
The Lagrange interpolation of
f(x) = b**x / x!at consecutive integer nodesis a sum whose consecutive term ratio is a rational function of the node
index:
rho(t) = -b * (n1 - t) / (t * (a0 + t))times the ratio of two nodedistances. Such a sum is a "matrix factorial": the partial sum state advances
by the 2x2 matrix
[[den_t, 0], [num_t, num_t]]per node, and the product ofsconsecutive matrices is a polynomial in the batch startz. This is thesetting of Chudnovsky-Chudnovsky / Bostan-Gaudry-Schost (BGS).
The implementation is the value-domain variant of BGS ("shift of evaluation
values"):
S ≈ sqrt(2l)batches ofSnodes.P_s(z)by the values of its three entries(D, N, M)atz = u * s, not by coefficients.P_2s(z) = P_s(z) * P_s(z + s): extend the value tables by shiftingevaluation values (one convolution with exact binomial weights and a
small-integer reciprocal kernel), then combine pointwise. There is no product
tree and no separate multipoint evaluation step; the total cost is a
geometric sum over the doublings.
Convolutions run by Kronecker substitution onto Integer multiplication,
which is why GMP is required: with Toom-Cook the pipeline is asymptotically
worse than BSGS, and it stays off automatically without
Integer::GMP_VERSION.c_k * N_k / D_kper batch for the sum and oneD_kfactor per batch forthe product.
prod (x - i)that movesxabove2 * precuses thesame doubling with a single-entry table.
Numerical stability
The fixed-point tables lose precision through the dynamic range of the values
across batches, not through the extrapolation of the value shift: the
measured loss with zero guard bits is
1.35 .. 1.49 * S * n1.bit_lengthbits, stable over 300 .. 10,000 digits and identical for
xnext to a node.The guard is set to
2 * S * (n1.bit_length + 4) + 256bits.An
xwithin10**-qof an interpolation node cancelsqdigits in onebatch denominator. The batch factor of
prodis derived from the samecomputed
D_kthat the sum divides by, so this cancellation is exact inprod * sum, the same contract as thebatch_prodreuse in the BSGS branch.Measured against BSGS at 3000 .. 4100 digits with
xwithin10**-(prec-10)of a node, the error stays below 0.5 ulp.What is in the branch
lib/bigdecimal/math/gamma_multipoint.rb(345 lines): the engine, plusMultipoint.use?,Multipoint.enabled(kill switch) andMultipoint.min_prec(3000; the measured crossover against BSGS is~2500 digits).
lib/bigdecimal/math/gamma.rb: a one-line dispatch at the top ofGamma.gamma_lagrange. Reflection (x < 0.5),lgammaand the factorialdoubling all reach the engine through this single point.
test/bigdecimal/test_bigmath.rb: convergence tests that cross the dispatchthreshold, including an
xnext to a node.gamma_mp_check.rb: accuracy sweep and benchmark against BSGS(
Multipoint.enabledtoggle).incgamma_mp_check.rb: a second client of the same doubling driver, theincomplete gamma series
gamma(a) ≈ r**a * e**-r / a * (1 + sum prod r/(a+i))for full-digit
ain[0.5, 3]. Its constant numerator degenerates the 2x2matrix to two tables, and it runs 1.4x .. 2.3x faster than the Lagrange
engine at 5,000 .. 50,000 digits with exact agreement. Left as a script; it
suggests that a future lane split (incomplete gamma series for small
x,Lagrange only for the duplication formula at large
x) would be simpler andfaster than this PR's single engine.
History of the experiment
The commit sequence records the path, including the parts that were retired:
evaluated per point by Horner (O(prec^1.5 · log² prec)).
~250,000 digits, so it never won in practice.
formula fitted to measured loss.
four (50,000 digits: 92s to 70s).
Gamma.gamma_lagrange.the value domain, then the coefficient-domain engine removed: one engine,
one error model, one code path (650 to 345 lines).
Why this is not being merged
BSM and BSGS branches cover the common cases and stay unchanged.
guard formula. It is verified over 300 .. 50,000 digits, not proved, and the
guard has not been re-checked for
S > 1000(above ~10^6 digits).digits.
measurements are in the commit messages, so the branch can serve as a
starting point if someone wants to pick it up.
Try it