Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Fix rounding of BigInteger conversions to floating-point types - #130565

#130565
Merged
tannergooding merged 1 commit into
dotnet:maindotnet/runtime:mainfrom
tannergooding:tannergooding-sturdy-carnivaltannergooding/runtime:tannergooding-sturdy-carnivalCopy head branch name to clipboard
Jul 13, 2026
Merged

Fix rounding of BigInteger conversions to floating-point types#130565
tannergooding merged 1 commit into
dotnet:maindotnet/runtime:mainfrom
tannergooding:tannergooding-sturdy-carnivaltannergooding/runtime:tannergooding-sturdy-carnivalCopy head branch name to clipboard

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Fixes #49611
Fixes #125837

(double)BigInteger

The explicit operator double(BigInteger) truncated toward zero rather than rounding. It gathered the top mantissa bits and passed them to NumericsHelpers.GetDoubleFromParts, which masked off the low bits (truncation) and never observed the lower limbs (no sticky bit). So (double)(BigInteger)(long.MaxValue / 2) gave 4611686018427387392 where the direct (double)(long)(long.MaxValue / 2) correctly gives 4611686018427387904.

The conversion now performs IEEE round-to-nearest, ties-to-even: it builds the top 64 significant bits (man, MSB at bit 63), the top-bit position, and a sticky flag from any uncaptured lower bits/limbs, then keeps 53 bits using the round bit + sticky, handling the rounding carry and overflow to infinity. Both the 64-bit and 32-bit limb paths are covered. A BigInteger backed by a bits[] array is always a normal double magnitude, so there is no denormal/zero handling.


(float), (Half), (BFloat16)

These operators previously converted via double as an intermediate, which double-rounds and can be off by 1 ULP. In a sweep, float had hundreds of such errors; BFloat16 shares float's exponent range and is affected the same way. Half is not (every finite Half-range integer is exact in double), but is routed through the shared path anyway for uniformity.

The fix routes all three through a shared ConvertToDouble(value, roundToOdd: true) helper that rounds to odd at 53 bits. A round-to-odd intermediate is always innocuous when re-rounded to a narrower type, so the narrow result is correctly rounded.

This resolves the BigInteger portion of #112474; the Integer -> Half, Decimal -> FP, and Decimal32/64/128 -> FP items there remain out of scope.

Tests

Added rounding coverage to cast_from.cs: double ties-to-even boundaries, a deep lower-limb sticky bit, carry overflow to infinity, and float/Half/BFloat16 cases (the float/BFloat16 ones fail without the round-to-odd routing).

Note

This PR description was drafted with the assistance of GitHub Copilot.

The `explicit operator double(BigInteger)` truncated toward zero instead of
rounding, so values such as `long.MaxValue / 2` produced a result 512 below the
correctly rounded double that `(double)(long)` yields. Rewrite the conversion to
perform IEEE round-to-nearest, ties-to-even using the top 64 significant bits, a
round bit, and a sticky bit gathered from the uncaptured lower bits and limbs,
handling the rounding carry and overflow to infinity. Remove the now-unused
`NumericsHelpers.GetDoubleFromParts`.

The `float`, `Half`, and `BFloat16` operators previously converted via `double`
as an intermediate, which double-rounds and can be off by 1 ULP. Route them
through a shared `ConvertToDouble` helper that rounds to odd at 53 bits, which is
innocuous when re-rounded to the narrower type, giving a correctly rounded
result.

Fixes dotnet#49611
Fixes dotnet#125837

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Numerics.BigInteger floating-point conversion operators to produce correctly-rounded results, fixing truncation in (double)BigInteger and avoiding double-rounding errors in (float), (Half), and (BFloat16) conversions.

Changes:

  • Reworked (double)BigInteger conversion to implement IEEE 754 round-to-nearest, ties-to-even using a 64-bit significand window plus a sticky-bit scan of lower limbs.
  • Routed (float), (Half), and (BFloat16) conversions through a shared ConvertToDouble(..., roundToOdd: true) path intended to prevent downstream double-rounding when narrowing.
  • Added new rounding-focused tests covering tie-breaking, deep-limb sticky bits, overflow-to-infinity, and float/BFloat16 double-rounding-sensitive cases.
Show a summary per file
File Description
src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Replaces double/float/Half/BFloat16 conversion logic with a shared ConvertToDouble implementation that accounts for rounding and sticky bits.
src/libraries/System.Runtime.Numerics/src/System/Numerics/NumericsHelpers.cs Removes the now-unused GetDoubleFromParts helper.
src/libraries/System.Runtime.Numerics/tests/BigInteger/cast_from.cs Adds regression and boundary tests validating correct rounding behavior and previously failing double-rounding scenarios.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

@tannergooding
tannergooding merged commit 4740bd8 into dotnet:main Jul 13, 2026
91 checks passed
@tannergooding
tannergooding deleted the tannergooding-sturdy-carnival branch July 13, 2026 13:25
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jul 14, 2026
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Fixes #49611
Fixes #125837

## `(double)BigInteger`

The `explicit operator double(BigInteger)` truncated toward zero rather
than rounding. It gathered the top mantissa bits and passed them to
`NumericsHelpers.GetDoubleFromParts`, which masked off the low bits
(truncation) and never observed the lower limbs (no sticky bit). So
`(double)(BigInteger)(long.MaxValue / 2)` gave `4611686018427387392`
where the direct `(double)(long)(long.MaxValue / 2)` correctly gives
`4611686018427387904`.

The conversion now performs IEEE round-to-nearest, ties-to-even: it
builds the top 64 significant bits (`man`, MSB at bit 63), the top-bit
position, and a `sticky` flag from any uncaptured lower bits/limbs, then
keeps 53 bits using the round bit + sticky, handling the rounding carry
and overflow to infinity. Both the 64-bit and 32-bit limb paths are
covered. A `BigInteger` backed by a `bits[]` array is always a normal
double magnitude, so there is no denormal/zero handling.

----------

## `(float)`, `(Half)`, `(BFloat16)`

These operators previously converted via `double` as an intermediate,
which double-rounds and can be off by 1 ULP. In a sweep, `float` had
hundreds of such errors; `BFloat16` shares `float`'s exponent range and
is affected the same way. `Half` is not (every finite `Half`-range
integer is exact in `double`), but is routed through the shared path
anyway for uniformity.

The fix routes all three through a shared `ConvertToDouble(value,
roundToOdd: true)` helper that rounds to odd at 53 bits. A round-to-odd
intermediate is always innocuous when re-rounded to a narrower type, so
the narrow result is correctly rounded.

This resolves the `BigInteger` portion of #112474; the `Integer ->
Half`, `Decimal -> FP`, and `Decimal32/64/128 -> FP` items there remain
out of scope.

## Tests

Added rounding coverage to `cast_from.cs`: `double` ties-to-even
boundaries, a deep lower-limb sticky bit, carry overflow to infinity,
and `float`/`Half`/`BFloat16` cases (the `float`/`BFloat16` ones fail
without the round-to-odd routing).

> [!NOTE]
> This PR description was drafted with the assistance of GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
tannergooding added a commit that referenced this pull request Jul 16, 2026
A performance follow-up to #130565 (which fixed the *correctness* of
`BigInteger` -> floating-point conversions). No behavioral change -- the
results are identical, this only makes them faster. Validated against a
2M-value oracle sweep vs `double.Parse`/`float.Parse` (0 mismatches)
plus the existing rounding tests.

## `ulong` hardware fast path

Magnitudes that fit in 64 bits are by far the common case. Rather than
run them through the general limb scan, `operator double`, `operator
float`, and `operator BFloat16` now reconstruct a `ulong` (`ToUInt64`)
and use the correctly rounded `(T)(ulong)` conversion the hardware
already provides. `(double)(ulong)`, `(float)(ulong)`, and `BFloat16`'s
direct integer rounding are all correctly single-rounded, so this is
exact. `Half` has no fast path because every `bits[]`-backed magnitude
(>= 2^31) overflows it to infinity anyway.

----------

## Round directly at the target width

The narrow-type operators previously rounded to odd at 53 bits and then
re-rounded via the narrowing cast, to dodge double rounding. Instead,
`ConvertToDouble` now takes the target `precision` (53 for `double`, 24
for `float`, 11 for `Half`, 8 for `BFloat16`) and rounds a single time
at that width using round-to-nearest, ties-to-even. The resulting
`double` already lies exactly on the target type's grid, so the
narrowing cast is exact and no double rounding is possible. Round-to-odd
is gone entirely.

This is valid precisely because every `bits[]`-backed `BigInteger`
magnitude is at least 2^31, comfortably in the normal range of all four
targets, so a target subnormal (the only case that would need extra
intermediate precision) never occurs.

A side benefit: the narrow types now share `double`'s sticky-scan gate.

----------

## Lazy sticky scan

The lower limbs only affect the result when the top bits sit exactly on
the halfway point (the round bit is set) and nothing above already made
the value sticky. `ConvertToDouble` now scans the lower limbs (an O(n)
walk) only in that case, instead of always. Since
`Half`/`float`/`BFloat16` now use the same round-to-nearest gate as
`double`, they benefit too.

## Numbers

Local A/B (Release corerun, identical CoreLib/JIT, base = current main,
this = PR). ns/op:

| Case (magnitude) | double base -> PR | float base -> PR |
| --- | --- | --- |
| Small (<= 64-bit) | 4.23 -> 1.57 (0.37x) | 4.40 -> 1.57 (0.36x) |
| 65-bit / ~128-bit | ~neutral | ~neutral |
| Large (~1024-bit) | 7.15 -> 4.09 (0.57x) | 7.22 -> 4.08 (0.57x) |

The common <= 64-bit case is ~2.7x faster for both, and large-magnitude
`float` is now at parity with `double` (was 7.2 vs 4.1ns).

## Tests

Added wide (> 64-bit) rounding cases for `double`, `float`, and
`BFloat16` to `cast_from.cs` so the general limb-scan path stays covered
now that the <= 64-bit killer values hit the fast path.

> [!NOTE]
> This PR description was drafted with the assistance of GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BigInteger conversions to float, Half, and BFloat16 risk double rounding Conversions from BigInteger to Double are not always as precise as from Long

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.