floating_vs_decmial_x design
Design goal
The package compares moonbitlang/x/decimal (X) and
Luna-Flow/floating/decimal_gda@0.7.1 (GDA) on the same inputs. The two
libraries do not implement the same arithmetic: X keeps at most 28 fractional
digits and truncates, GDA rounds to a significant-digit precision chosen by a
context. A fair comparison therefore has to fix a semantic contract first, make
both libraries meet it, prove that they meet it, and only then measure.
The API page lists the items, the tutorial runs them, and the measured numbers are in the performance chapter. The method shared with the DzmingLi benchmark (oracle verdicts, canonical form, paired statistics) is derived on the dzmingli_vs_floating design page; this page states what differs.
Mathematical background
Two decimal models
An X decimal is a pair with denoting
; Decimal::new rejects other scales. Its operations are
where is integer division truncating toward zero and truncates toward zero to fractional digits, as defined on the DzmingLi design page. Addition, subtraction and comparison are exact. Because , the exponent is never negative, so
exactly: X’s quotient is the true quotient truncated to 28 fractional digits.
A GDA operation in a context of precision with rounding Down returns
, the exact result truncated to significant
digits; quantize(y, 10^{-28}) in the same context returns
when the result fits in digits.
Nested truncation
Two facts about truncation carry the proofs below.
Lemma 1. For and every real , .
Proof. Let (the negative case is symmetric) and . is the largest point of not above . Since , the point lies in and , so . The largest point of below is therefore at least and at most the largest point of below , which is .
Lemma 2. For integers and , .
Proof. For write with and with . Then and , so . Truncation toward zero is odd in , which gives the negative case.
Design decisions
Two semantic groups
Problem. For multiplication and division the libraries’ native results
differ as soon as a result needs more than 28 fractional digits, and timing
two different computations says nothing. Options. Restrict the benchmark to
inputs where both are exact; force GDA to reproduce X’s policy; force X to
reproduce GDA’s. Choice. Two groups, reported separately:
ExactOverlap restricts the inputs, XCompatible makes GDA reproduce X’s
policy with an extra quantize. Why. X has no context to configure, so
only the GDA side can adapt; and the cost of that adaptation is part of what a
GDA user pays for X’s semantics. The two groups carry different timing scopes,
arithmetic_only and semantic_equivalent_pipeline, and are never
aggregated.
One oracle for both groups
The oracle implements X’s policy: exact addition, subtraction and comparison, of the exact product when its scale exceeds 28, and of the quotient. For division with it returns when and otherwise, which equals by Lemma 2 (taking the sign of into ). Both cases are .
In ExactOverlap the truncation is the identity on every generated result, so
the same oracle returns the exact value:
- multiplication fixtures use scale pairs with ;
- division fixtures divide an integer by , , , or , whose reciprocals , , , and have at most three fractional digits;
- addition, subtraction and comparison are exact by definition.
Precision contracts
The GDA precision is working_precision(op, ℓ, r, semantics) with no
guard digits added. With the coefficient digit counts and
:
so every ExactOverlap result, and the XCompatible product before
quantization, is exact in GDA. The XCompatible product is then quantized to
when ; the quantized coefficient has fewer digits
than the exact one, so quantize succeeds and returns
of the exact product, which is X’s product.
For XCompatible division the precision is
bounds the number of integer digits of the quotient:
divide returns . If it has at
most integer digits, so at least fractional digits survive;
if every kept digit is fractional and at least of them
survive. In both cases
on a grid with , and Lemma 1 gives
The quantized coefficient has at most digits, so quantize
does not raise an invalid operation. The two extra digits beyond the 28 are
guard digits; Lemma 1 needs none, but they keep the contract independent of
how the quotient’s last digit is produced.
This derivation assumes that divide receives the exact operands and .
The next section shows when the fixture breaks that assumption.
Known limitation: operand rounding in X-compatible division
prepare_fixture parses the GDA operands with
Decimal::from_string(text, precision=p), which rounds half-even to
significant digits. For XCompatible division depends on the digit
difference , not on the operand lengths, so operands longer than digits
are rounded before the timed division runs. Two consequences follow.
- Correctness is not guaranteed by construction. With operands rounded to relative error at most each, the computed quotient differs from by up to about , which can move a quotient across a multiple of . For example and give ; GDA parses as and returns , while X and the oracle return . The published corpora pass validation because their quotients stay away from such boundaries; that is an empirical result for the recorded seed.
- The timed work differs. The fixtures pair operands with the same digit
count and , so from on GDA divides operands of at
most 51 digits while X divides the full -digit operands. The
x_compatibledivision timings at 64 digits and above therefore do not compare equal work. The common-digit run () is not affected.
Parsing the operands with a precision of at least and dividing in a context of precision would remove both effects. The benchmark code is unchanged on this branch; the limitation is recorded here and in the performance chapter.
Fixtures excluded from timing
Conversions to X and GDA values, the quantum, the context,
validation and canonicalization are all built or run outside timing. The timed
body is run_x or run_gda: one public operation, plus the quantize step in
the XCompatible pipeline.
Paired statistics
The pairing, the relative delta (here relative to the X median), the
decision threshold and the speedup ratio are those of the
DzmingLi benchmark, with X as the
baseline: x_speedup_vs_gda above means X is faster.
Correctness and invariants
- Canonical form and the soundness of the zero tolerance are proved on the DzmingLi design page; the neutral-model code is identical.
ExactOverlap: every generated result is exact in both libraries and equal to the oracle, by the precision table and the identity of on those results.XCompatiblemultiplication: X, GDA and the oracle all return of the exact product.XCompatibledivision: X and the oracle return for all inputs. GDA does so whenever its operands have at most significant digits (Lemma 1); longer operands are the known limitation above.- Comparison: X’s
Compare::comparereturns the sign of a coefficient comparison at a common scale, whichnormalize_ordermaps to ; GDA’scomparereturns the same values as decimals.
Alternatives rejected
- A single “fair” semantics for all operations. Rejected: there is none that both libraries implement natively for products and quotients beyond 28 fractional digits.
- Making X imitate GDA. Rejected: X has no precision or rounding context, so it would need extra rescaling outside its public API.
- Comparing with a tolerance of one unit in the 28th place. Rejected: both sides truncate deterministically, so exact agreement is required and achievable.
- One combined score per operation. Rejected:
arithmetic_onlyandsemantic_equivalent_pipelinetime different work.
Boundaries
- Only add, subtract, multiply, divide and compare are measured. X has no context, flags, special values or exponent cohorts, so none of those are compared.
- The
XCompatibledivision contract holds only for operands that GDA parses exactly; see the known limitation. - The HTML corpus summary reports total
validation_count + failed_countand passedvalidation_count. Mare Mark’svalidation_countalready includes failures, so the summary overstates both numbers whenever a run has failures. The published runs had none. - The Mare Mark implementation record names
moonbitlang/x@0.4.46, while the module now depends onmoonbitlang/x@0.5.5; the published measurements were taken with 0.4.46. BigInt::from_stringreturns wrong values for long inputs on thewasm-gctarget of the currentmoonbitlang/core; a string of 3,584 nines already parses wrongly, and 4,096 nines parse to a 4,094-digit number. The package does not use it; the testdivision precision follows the requested semantic contractdoes, and its expected value4097was recorded from that wrong parse. The correct value is4099, which thenativeandjstargets compute.- The executables run on the
nativetarget only, and results from different targets are never combined.