Decimal: use UInt128 significand to speed up operations#2022
Open
xwu wants to merge 18 commits into
Open
Conversation
Decimal: use UInt128 significand to speed up comparison and additionDecimal: use UInt128 significand to speed up operations
xwu
commented
Jun 4, 2026
added 3 commits
June 4, 2026 20:20
…w, fix off-by-one exponent in constants, plumb through loss-of-precision
…of UInt128 helper extensions
Author
|
@swift-ci test macOS |
Author
|
cc @stephentyrone :) |
xwu
commented
Jun 6, 2026
This comment was marked as outdated.
This comment was marked as outdated.
xwu
commented
Jun 6, 2026
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
xwu
commented
Jun 10, 2026
| let result = try lhs._multiply(by: rhs, roundingMode: .plain) | ||
| lhs = result | ||
| } catch _CalculationError.underflow { | ||
| lhs = .zero |
Author
There was a problem hiding this comment.
Note that this may be formally tantamount to a policy change as compared to NSDecimal* guarantees, but is also probably the more (only?) reasonable behavior.
The prior implementation never threw .underflow, so there is no actual precedent for this specific operation. In practice, that implementation also had sufficient issues with correctness, precision, and not respecting rounding mode that I'm not sure users could rely upon it to produce zero or NaN (or sometimes a totally unspecified arbitrarily large result—see above).
It is already the behavior in existing code with respect to at least some operations to underflow to zero:
Author
|
@swift-ci test macOS |
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.
This PR introduces a new internal computed property (called
_significandto distinguish itself from_mantissa) of typeUInt128, which allows us to perform arithmetic operations bypassingVariableLengthInteger.Although conceptually low-hanging fruit, fully threading the changes through the implementation represents an overhaul of some scale but with performance gains to match. The resulting implementations (written by hand) are fortunately imminently readable. Latent bugs are addressed along the way, substantially improving the precision of mathematical operations on
Decimal.Motivation:
#1754 demonstrated that making
VariableLengthIntegernon-allocating (and not really variable) dramatically improves performance. While improved, however,Decimaloperations are still by no means optimized for performance. Sadly, this state of affairs encourages the erroneous impression that decimal floating-point is intrinsically much less performant than it could be as compared to alternative numeric representations.The prior PR was a fantastic and inspiring first move. However, absent context about other advances in Swift, LLM-driven efforts overlook that performing arithmetic limb-by-limb (which is what
VariableLengthIntegerencapsulates) is no longer necessary for implementing basic operations, as the 128-bit mantissa can be bitwise copied into aUInt128so that we can leverage more performant compiler primitives.Modifications:
This PR replaces
VariableLengthIntegeroperations withUInt128operations, rewriting comparison, addition (and subtraction), multiplication, and division. Normalization is also rewritten to remove the last consumer ofVariableLengthInteger, but it is also now only called by theNSDecimalNormalizeshim.Along the way, latent bugs are either annotated or fixed altogether--see added tests. For example:
The existing implementation truncates the 'refitted' mantissa in the case of arithmetic overflow during addition, which is not correct for the documented default
.plainrounding mode (it also makes no attempt to behave correctly for other rounding modes). The revised implementation now respects rounding mode.The existing implementation exhibits unexpected behavior when multiplying two values with small exponents that should lead to an underflow result. (Reading the code suggests there should be a runtime trap, but in the REPL there's just a very large arbitrary result.) The revised implementation now correctly throws
underflow.The existing implementation always rounds towards zero (i.e., truncates) for division. The revised implementation now respects rounding mode (crucially, the documented default rounding mode,
.plain).The existing implementation normalizes dividend and divisor by an arbitrary criterion chosen in 1999, which has been associated with bugs; code comments reference rdar://problem/5197585 and rdar://problem/2354750. The revised implementation now scales the dividend's significand appropriately to fill 128 bits.
The existing implementation produces a NaN value during normalization if the smaller of the two inputs has a finite, negative value that truncates to zero. The revised implementation now respects rounding mode and, if rounding up such a negative value, produces zero rather than spurious NaN.
In the existing implementation, legacy
NSDecimal*functions other thanAddnever signal loss of precision, as such information was neither consistently computed nor plumbed through. The revised implementation now indicates loss of precision whenever an inexact result is returned.Result:
Using benchmarks added in #1754, this PR results in a
~350%~500% boost in addition performance, a~750%~950% boost in multiplication performance, and a ~7000% boost in division performance as measured by throughput.And, as described above, arithmetic operations now have improved precision and latent bugs have been fixed.
VariableLengthIntegeris removed entirely.Testing:
All 33 existing unit tests for
Decimalpass (with modifications to account for now-corrected rounding with division and improved precision--see comments below). Additional unit tests are added for corrected behavior.All 5 existing benchmark tests show improved performance compared to the current baseline as described above.