dzmingli_vs_floating design
Design goal
The package answers two questions about DzmingLi/decimal@0.2.2 and
Luna-Flow/floating/decimal_gda@0.7.1: do they compute the same exact decimal
results, and how fast do they compute them as coefficients grow to thousands of
digits? Speed is only reported where both answers are right, so the design is
built around a correctness check that cannot be fooled by the libraries
agreeing with each other.
The API page lists the items, and the tutorial runs them. Measured numbers are in the performance chapter.
Mathematical background
Decimal values
A finite decimal is a pair that denotes
The map is not injective: and denote the same number. GDA arithmetic keeps such pairs apart (the exponent is part of the result), while the benchmark compares numbers. It therefore needs a canonical representative of each class; the package uses the pair with and the fewest trailing zeros in .
For write for the number of decimal digits of , so that
working_precision computes as the length of the decimal string of
, which gives .
Precision and rounding toward zero
A GDA context with precision represents exactly the numbers
with and in the exponent range. An
operation computes the exact result and rounds it to such a number. With
rounding mode Down (toward zero) the result is
so , with equality exactly when has at most significant digits. The same truncation at a fixed number of fractional digits is written
Differential testing with a reference oracle
Let be the implementations under test, an oracle and a canonicalization map into a set where equality is decidable. For an input the verdict of implementation is
Pairwise differential testing checks only . That check is blind to common-mode errors, where both implementations share a wrong result, and when it fails it does not say which side is wrong. With an oracle each implementation is judged on its own, and a disagreement between the two is explained by the verdicts.
Design decisions
Three implementations, two of them measured
Problem. The benchmark has to separate “fast” from “fast and wrong”.
Options. Pairwise agreement between the libraries; one library as the
reference for the other; an independent oracle. Choice. An independent
exact oracle over BigInt, run outside timing on every dataset
(ValidationCoverage::EveryDataset). Why. The oracle is small enough to
review line by line, shares no code with either library, and it found the 108
DzmingLi failures from 4,096 digits on, which pairwise agreement would have
reported only as “the libraries differ”. A size enters the speedup plots only
when every validation at that size passed for both implementations.
A neutral representation and a zero tolerance
Problem. The two libraries print results differently (exponent notation,
trailing zeros, sign of zero), and approximate comparisons would hide real
errors. Choice. Every result is parsed into DecimalValue and compared by
canonical_string equality, a tolerance of exactly zero. Why. All
benchmarked operations have an exactly representable result under the
precision contract below, so a correct implementation must reproduce it
digit for digit; the soundness argument is in
Correctness and invariants. The price is that
the comparison ignores the exponent cohort and the status flags; the official
decTest audit (tools/run_dzmingli_dectest_audit.sh) covers those.
The exact-finite oracle
The oracle uses only integer operations on coefficients.
Addition and subtraction align both operands to :
Multiplication multiplies coefficients and adds scales. Division writes the quotient as a fraction of integers and reduces it,
and accepts it only if has no prime factor other than and . It then finds the least with and returns . Integer division, remainder, power, square root of perfect squares, FMA and the unary operations follow from these rules; the table on the API page lists them.
The truncating rules (DivideInteger, Quantize, Rescale,
ToIntegralExact, ToIntegralValue) truncate toward zero because
BigInt division does. GDA’s quantize, rescale and to_integral_*
round with the context’s mode, so both contexts use Down; any other mode
would make the oracle wrong for those operations. In the published fixtures
these operations are exact anyway (see the next decision).
Precision contract
Problem. Every operation must be performed at a precision that holds
the exact result, so that rounding never happens and the zero tolerance is
fair. A larger is not free: the work of a GDA division can grow with the
precision, so an oversized could time work that the result does not need.
Choice. is computed per fixture from the operands, as the smallest
simple bound on the number of digits of the exact result plus guard digits:
working_precision(op, ℓ, r) + 2, with overrides for Power and Fma.
Why. The derivations below show the bound holds for every operand class the
runners generate, and that it also holds every operand exactly, so parsing at
never rounds.
Write for the operand digit counts and
. The runners use these operands:
Add, Subtract, Multiply, Fma, Quantize and Compare take generated
operands with scales from the profiles , , ;
Divide, DivideInteger and Remainder divide a generated operand with scale
, or by , or ; Power squares (exponent );
SquareRoot takes the square of a generated integer; Quantize uses the
quantum , Rescale the exponent and ScaleB the
shift ; the remaining unary operations take integers.
Every in the table is at least , so the operands
themselves parse exactly. The Power and Fma overrides exist because
working_precision alone gives , which is for
the exponent and too small to hold a square.
The contract is proven for these operand classes, not for every input. For a general terminating divisor the quotient coefficient is , which has about digits, while the bound grows only by . At the bound fails: needs digits, and . Such a fixture is not silently accepted; it is rejected by the oracle comparison, as the next section shows.
Timing scopes
OperationOnly (arithmetic_only) times one public operation on operands that
were parsed before timing. FullPath (full_path) also parses the canonical
operand strings under the prepared context, which is what a caller that
receives text pays. Both scopes run the same datasets with the same
fingerprints, so their numbers describe the same inputs. Context construction,
canonicalization, validation and reporting stay outside both scopes.
Paired statistics
Mare Mark records, for every dataset , repetition and block , one calibrated latency per implementation. The package pairs DzmingLi and GDA samples that share and forms differences
If a sample is modelled as ,
with a drift shared by the block (frequency changes, cache state),
then :
the block effect cancels. BalancedBlocks alternates which implementation runs
first, so order effects cancel on average as well. The reported quantities are
and the decision is gda_faster when , dzmingli_faster when
, and equivalent otherwise. Medians have a breakdown point of
50 %, so the outliers that Mare Mark reports but does not remove cannot move
them far. The threshold is a practical-significance margin, not a
hypothesis test, and the report carries no confidence interval.11 Mare Mark’s compare_paired stores the interquartile range of the
as its interval. It describes the spread of the paired
differences, not the uncertainty of the median.
With three datasets per size and 20 confirmatory repetitions each, a valid size has 60 pairs.
Operation-specific size ceilings
DzmingLi’s digit_count evaluates bit_length * 30103 in a 32-bit Int.
The product overflows when
A product of two -digit operands has up to digits, so multiplication, FMA and squaring can cross the limit from . The scaling runner therefore stops all operations at 10,000 digits and runs only add, subtract, divide and compare at 16,384 and 20,000 digits. These are analytic bounds from the overflow condition; runs at 32,768 and 65,536 digits reproduced the abort.
Correctness and invariants
Canonical form. For , normalize returns with
and ( or ), and because
each step replaces by . Two such pairs denote the same
number only if they are equal:
which contradicts the normal form of ; and forces
. canonical_string writes the sign, the digits of and the
point position , all of which are determined by the number, so string
equality is number equality.
Oracle division. is in lowest terms. If then, since , , so has no prime factor other than and . Conversely, if , the least such is . The oracle checks the factorization before the loop, so the loop terminates, and the returned scale is the shortest exact one.
Integer square root. The oracle iterates
where is the digit count of . By the AM-GM inequality , so every iterate is at least ; while we have and hence . The sequence decreases strictly until it reaches , where the loop stops. The oracle then requires , so a non-square input aborts instead of producing a rounded root.
Soundness of the zero tolerance. Let be the exact result and the fixture precision.
- If has at most significant digits, a conforming GDA operation returns a number equal to , because is its own correctly rounded value in every rounding mode. Then : a correct implementation is never rejected.
- If has more than digits, rounding toward zero gives , so the canonical strings differ and the validation fails: insufficient precision is never accepted.
- Equality of canonical strings is equality of numbers, so no wrong result is ever accepted.
The precision contract establishes the premise of (1) for every generated operand class. Outside those classes (2) still holds, which makes the check fail-closed.
Determinism. Operands are derived from Mare Mark’s seeded
derive_seed(seed, "<operation>:<digits>", profile), never from the clock, so
the same seed reproduces the same corpus and the same fingerprints.
generate_decimal uses only the digits ; generated coefficients
therefore have exactly the requested number of digits and no trailing zeros.
Cost of the oracle. With -digit coefficients, addition is dominated by
the alignment multiplication by , multiplication by one
BigInt product, and division by a GCD and
multiplications by , where . All of it runs
outside the timed region.
Alternatives rejected
- Pairwise agreement only. Rejected: it cannot attribute a disagreement and misses shared errors.
- One library as the oracle for the other. Rejected: the GDA library is one of the subjects, and the benchmark found DzmingLi failures that would then be indistinguishable from GDA failures.
- Comparison with a tolerance (ulps or relative error). Rejected: every benchmarked result is exact in decimal, so any nonzero tolerance would only hide errors.
- A fixed large precision such as digits. Rejected: it can inflate division work beyond the result and couples timing to an arbitrary constant.
- Random operands per run. Rejected: results must be reproducible and fingerprints stable across timing scopes.
- Scaling benchmarks for
exp,lnandlog10. Rejected: identity fixtures would not exercise their algorithms, and general results need an independent high-precision transcendental oracle. They are covered by the decTest audit only.
Boundaries
- The package checks numeric values only. Exponent cohorts, trailing zeros of results, status flags, NaN, infinities and signed zero are outside the comparison; the decTest audit covers them.
- Division is exercised with the terminating divisors , and only. Repeating quotients and large denominators are not benchmarked, and the oracle rejects repeating quotients.
ParseandFormatare identity paths. They do not test GDA formatting; the decTest audit records DzmingLi’s 329toScifailures separately.OperandShapeis a label; the corpus is three deterministic scale profiles per size, not a random distribution.- The precision contract is proven for the generated operand classes listed above, not for arbitrary inputs.
- The executables run on the
nativetarget only; other targets print a message and exit. DzmingLi/decimal@0.2.2is deprecated in favour ofmoonbit-community/decimal; the benchmark keeps it pinned for a historical comparison.
Footnotes
-
Mare Mark’s
compare_pairedstores the interquartile range of the as its interval. It describes the spread of the paired differences, not the uncertainty of the median. ↩