[python] Give the Python package Decimal128, and make its division the fastest of the three - #307
Merged
Conversation
forfudan
force-pushed
the
py-decimal128
branch
from
August 29, 2026 16:02
e8bf248 to
62fcde0
Compare
…e fastest of the three decimo.Decimal128 is the fixed-width type: 96 bits of coefficient and a scale from 0 to 28, in sixteen bytes that own nothing. It arithmetics, compares, hashes and rounds like Decimal, mixes with int, float and str on either side of an operator, and carries quantize, round, as_tuple, sqrt, exp, ln, log10, the trigonometry and the IEEE 754 interchange bytes. Its hash agrees with int, float, decimal.Decimal and Decimal. Its division was the one operation slower than decimal's, so it was rewritten. The quotient was built a digit at a time, which cost 226 ns and could run out of digits before the rounding position: 1 in 300 random pairs was wrong in the last place. It is now one wide division rounded from the remainder, on top of a 256-by-128 divider that takes Knuth's algorithm D over 64-bit limbs -- 22 ns against the 261 the generic UInt256 division costs. From Python: addition 46 ns against decimal's 73, multiplication 57 against 85, division 114 against 133, construction 116 against 136. An invoice of three quantized lines with tax is 633 ns against 705.
forfudan
force-pushed
the
py-decimal128
branch
from
August 29, 2026 16:31
62fcde0 to
fe005a3
Compare
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.
decimo.Decimalis arbitrary precision. This adds the other type to the Python package:Decimal128, 96 bits of coefficient and a scale from 0 to 28 in sixteen bytes that own nothing, withDec128as the shorter name the Mojo library uses.+,-,*,/, the six comparisons and the hash are C slots rather than dictionary entries. The hash agrees withint,float,decimal.DecimalandDecimal, so the four are interchangeable as dictionary keys.quantize,round,normalize,adjusted,compare,copy_sign,copy_abs,copy_negate,to_integral_value,same_quantum,max,min,fma,as_tuple,as_integer_ratio,to_eng_string,from_floatand theis_predicates are there, along withsqrt,cbrt,root,exp,ln,log10,log, all six trigonometric functions, and the IEEE 754 interchange bytes from #305. It copies, pickles and formats like a value.A mixed expression settles in the wider type:
Decimal128 + Decimalis aDecimaleither way round, since 29 digits and a scale of 28 both fit.Decimal's operand conversion recognises the fixed-width type andDecimal128's refuses it, which is what hands the expression to the reflected operator.Division was the one operation slower than
decimal's, so it was rewritten. The quotient was built a digit at a time, which cost 226 nanoseconds and could run out of digits before reaching the position the rounding needed:504572829922.89957 / 525211.7899came out one unit low, and 1 in 300 random pairs was wrong in the last place. It is now one wide division rounded from the remainder, which is what settles a tie.That needed a divider. A divisor past 64 bits fell through to
UInt256 // UInt256, a software shift-subtract loop of 261 nanoseconds, soudiv_u256_by_u128takes Knuth's algorithm D over 64-bit limbs and does it in 22: every division inside it has a divisor that fits 64 bits, which__udivti3does in 2.6 nanoseconds against 75 for a full 128-bit one.Decimal128division is 70 nanoseconds against 226, and right on all 300 pairs.Nanoseconds per operation, from Python:
Decimal128Decimaldecimala + ba * ba / bstr(x)The invoice is three lines quantized to cents with tax on the subtotal.
stris the one still slower: 60 nanoseconds of that is the Mojo side and the rest is the call, so beating 63 means making the text itself about four times cheaper.Two bugs turned up in the operators. A failed conversion left CPython's error indicator set, and a slot returning
NotImplementedwith an error pending has it surface later attached to whatever ran next --Decimal128(1) + Decimal(2)raisedOverflowError: bad argument type for built-in operation. And__rpow__and__rdivmod__were missing, so2 ** Decimal128(3)anddivmod(7, Decimal128(2))were type errors.Three differences from
Decimalare in the README. Division fills the type -- 29 significant digits and a scale of at most 28 -- rather than following the context precision. A value past7.9E+28raises instead of rounding into range. And a scale is never negative, soDecimal128("1.23E+5")is123000: there is no cohort member with an exponent of 3 to print as123E+3.1.44 million random pairs check the divider against plain division, including both sides of the 64-bit boundary, a divisor larger than the dividend, and exact division. 2100 checks across every function in the
decimal128module against CPython'sdecimaland a 140-digit trigonometric reference: none wrong. The suite is at 1227 tests, and the Python tests cover the arithmetic, the method surface, hashing, money rounding, mixed expressions, the interchange bytes and what the type refuses.