Skip to content

Make DegreesToRadians and RadiansToDegrees correctly rounded - #132096

Open
tannergooding wants to merge 4 commits into
dotnet:mainfrom
tannergooding:tannergooding-fantastic-goggles
Open

Make DegreesToRadians and RadiansToDegrees correctly rounded#132096
tannergooding wants to merge 4 commits into
dotnet:mainfrom
tannergooding:tannergooding-fantastic-goggles

Conversation

@tannergooding

@tannergooding tannergooding commented Aug 10, 2026

Copy link
Copy Markdown
Member

x * (pi / 180) rounds twice -- once forming the constant, once forming the product -- leaving up to ~1 ulp of error. Over all 2^31 finite positive float inputs the current shape misrounds 37.9% of them, and 0.042% are off by 2 ulp.

Carry the factor as a multi-limb value and fold the limbs back with a single rounding. Two limbs suffice for all but a rare input, and whether they did is itself decidable, so the third limb is only paid for when needed; a scaled path covers the subnormals.

Covers float, double, Half, BFloat16, Vector64/128/256/512, Vector<T>, and Decimal32/64/128.

Fixes #131930


Validated against exact rational arithmetic: exhaustively for Half, BFloat16, float (all 2^31) and Decimal32 (60M); for double and the larger decimals against an enumerated near-tie set (372k vectors) plus replay of the whole subnormal band. 0 misrounded in every case.


Cost in ns per operation, over a 4096 element double array storing to a destination, so a Vector256 entry is one call handling four elements. Ad hoc harness on a 7950X, and both directions measure the same within noise.

before after
scalar 0.90 1.02
Vector128 0.88 1.81
Vector256 0.85 1.92
Vector512 1.65 3.00

The old code was bound by vdivsd / vdivpd throughput rather than by the multiply, which is why it cost about the same at every width. The new code has no divide and is FP issue bound instead, so it scales with the work rather than with the divider. Vector512 is double pumped on this part, hence roughly twice Vector256 in both columns.

Inputs below the point where the two limb form stops working take a scaled path, at roughly 13 ns.


  • The vector paths use Vector*.FusedMultiplyAdd where the hardware has it and a Veltkamp split otherwise, since recovering the roundoff of a product requires a real fused multiply-add.
  • Decimal zero now carries the IEEE preferred exponent: Decimal32.DegreesToRadians(0E0) is 0E-17, was 0E-8.
  • The six new Decimal* members mirror the existing double / float ITrigonometricFunctions surface.

Note

Portions of this pull request were drafted with Copilot.

The conversion factor is not representable, so multiplying by a rounded
constant rounds twice and leaves up to a full ulp of error. Carry the
factor as a multi-limb value instead and fold the limbs back with a
single rounding, over the whole domain including the subnormals.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 10, 2026 20:08
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@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

Improves the correctness of the DegreesToRadians / RadiansToDegrees conversions across scalar and SIMD APIs by avoiding double-rounding and using correctly-rounded wide-constant multiplication (including a rare slow path and subnormal scaling for double), plus adding targeted edge/near-tie tests.

Changes:

  • Reworks double conversions to use a head+mid+tail multi-limb constant with decidable 2-limb fast path, 3-limb fallback, and a scaled path for very small inputs.
  • Reworks float (and Half/BFloat16) conversions to avoid overflow/inaccuracy by multiplying by the double head constant and rounding once.
  • Adds/updates SIMD and decimal IEEE-754 decimal helpers + extensive edge/near-tie tests for all affected types.
Show a summary per file
File Description
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs Adds bitwise edge-case tests for float degrees/radians conversions (zero/subnormals/overflow/sign).
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs Adds bitwise edge-case tests for BFloat16 conversions.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs Adds bitwise edge-case tests for Half conversions.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs Adds bitwise edge/near-tie tests for double conversions (min/max cutoffs + closest-midpoint cases).
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs Adds NaN/Inf/zero/extrema/subnormal + near-tie bit-pattern tests for Decimal64 conversions.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal32Tests.cs Adds NaN/Inf/zero/extrema/subnormal + near-tie bit-pattern tests for Decimal32 conversions.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal128Tests.cs Adds NaN/Inf/zero/extrema/subnormal + near-tie bit-pattern tests for Decimal128 conversions.
src/libraries/System.Runtime/ref/System.Runtime.cs Adds new Decimal32/64/128.DegreesToRadians and RadiansToDegrees members to the public ref surface.
src/libraries/System.Private.CoreLib/src/System/Single.cs Changes scalar float conversions to multiply by double head constants (single rounding, avoids intermediate overflow).
src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/VectorMath.cs Introduces wide-constant SIMD machinery for double and widened float conversions, including FMA-based roundoff recovery.
src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector64.cs Routes SIMD degrees/radians conversions to the new VectorMath paths, with widening selection for float.
src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs Routes SIMD degrees/radians conversions to the new VectorMath paths, with widening selection for float.
src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector256.cs Routes SIMD degrees/radians conversions to the new VectorMath paths, with widening selection for float.
src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector512.cs Routes SIMD degrees/radians conversions to the new VectorMath paths.
src/libraries/System.Private.CoreLib/src/System/Numerics/Vector.cs Routes Vector<T> degrees/radians conversions to the new VectorMath paths.
src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs Updates DIM commentary to clarify why interface-default implementations can’t generally be correctly rounded.
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs Adds Decimal32 conversion APIs implemented via wide-constant decimal helper.
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs Adds Decimal64 conversion APIs implemented via wide-constant decimal helper.
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs Adds Decimal128 conversion APIs implemented via wide-constant decimal helper and documents the constant layout rationale.
src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs Simplifies conversions to delegate directly to the updated float implementations.
src/libraries/System.Private.CoreLib/src/System/Half.cs Simplifies conversions to delegate directly to the updated float implementations.
src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs Adds a shared helper to multiply decimal IEEE-754 values by a split wide constant (single rounding) + optimizes digit dropping with chunked division.
src/libraries/System.Private.CoreLib/src/System/Double.cs Implements correctly-rounded double conversions using multi-limb constants, decidable fast path, 3-limb fallback, and subnormal scaling.

Review details

  • Files reviewed: 23/23 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/libraries/System.Runtime/ref/System.Runtime.cs
Copilot AI review requested due to automatic review settings August 14, 2026 13:50

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.

Review details

Suppressed comments (1)

src/libraries/System.Private.CoreLib/src/System/Double.cs:2189

  • The trailing comment is incomplete (ends mid-sentence), which makes this block harder to understand/maintain.
            // `sum` sat exactly between `result` and `next`, so the true value did not
  • Files reviewed: 23/23 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ed0d2b2ce19451f2

Copy link
Copy Markdown

I took a look at the binary scalar portion of this, and am glad to see interest in correctly rounded math.

For Half and BFloat16, it looks like you're doing:

  • convert to Single
  • convert to Double
  • multiply by Double constant
  • convert to Single
  • convert to original type

The following appears to be correctly rounded for Half, and I imagine it is for BFloat16 as well:

  • convert to Single
  • multiply by correctly rounded Single constant
  • convert to original type

For tests for Half and BFloat16, you might consider passing all 2^16 arguments (possibly with special handling or exclusion of NaNs; I'm not sure what you guarantee there) to the function in a reproducible order, hashing the output, and comparing to a reference hash value. Maybe you could do the same for some binades of Single, and some even smaller fraction of the domain for Double.

For Double, I did not try to convince myself of the correctness of the implementation, but the general idea seems plausible enough. It's unfortunate that fma(x, c1, x * c2) is not enough for these particular constants. I'm curious how the closest-to-midpoint cases 0x3FF9_6BDF_4AA9_CD3B and 0x3FFD_B0FB_3010_78BE were found, and for how many other arguments the fma(x, c1, x * c2) approach fails to be correctly rounded. It would probably make sense to include more near-midpoint cases in the tests, because even a not-particularly-accurate implementation might get a significant fraction them correct.

I'm also curious how the performance is without hardware FMA, and how the proposed approach compares to just calling FusedMultiplyAdd anyway.

If you're going to go with correctly rounded implementations here, it would be nice if the documentation promised that. Much of the benefit of correct rounding is unavailable if it's not a documented promise. I suppose you might not want to commit to that, in which case it would still be good to promise some limit on the inaccuracy, such as that the maximum ULP error is less than 1.

Copilot AI review requested due to automatic review settings August 15, 2026 16:32
@tannergooding

Copy link
Copy Markdown
Member Author

Reiteration of exactly what the PR is doing and/or what the original post describes is not beneficial.

Checking in a 65k entry dataset is likewise not beneficial, its the type of thing that is more than sufficient to cover via simple mathematics and an exhaustiveness test prior to check-in. If the algorithm changes in the future, devs should do the due diligence in testing additional scenarios.

The tests given are generally sufficient, covering some basic values, the well known special values, and various edge case scenarios that are common to showcasing floating-point 1-2 ULP errors.

The mid-point cases can be derived mathematically based on the known error from the head/tail and are covered in the code comments around those constants.

Using a Veltkamp/Dekker split is a well known scenario for when no-FMA exists because it explicitly does less work. This is important for platforms, like WASM, without FMA. It also impacts the default case for x64 NAOT since the baseline is still x86-64-v2 (SSE4.2+POPCNT)

We typically do not document exact precision guarantees of functions. They are simply documented when they defer to some other implementation.

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.

Review details

Suppressed comments (1)

src/libraries/System.Runtime/ref/System.Runtime.cs:11484

  • This PR adds new public API in the ref assembly, but the only linked issue in the PR description (#131930) does not have the api-approved label. If these APIs were approved via DIM/alternative process, please link the actual approval issue (with api-approved) or otherwise provide the standard approval signal so the API approval verification procedure can be satisfied.
  • Files reviewed: 23/23 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ed0d2b2ce19451f2

Copy link
Copy Markdown

Reiteration of exactly what the PR is doing

I left out the implicit question for Half and BFloat16: why are you multiplying in Double and converting back in two steps if Single is sufficient? Or am I wrong that it is?

Checking in a 65k entry dataset

The point of the hash suggestion was that you could check in a single hash value that covers any number of test cases. I am not in a position to say whether it makes sense in this context, but figured I'd mention the idea anyway.

We typically do not document exact precision guarantees of functions.

I'm aware. But if you can easily give a useful upper bound (like <1 ULP), why not?

Copilot AI review requested due to automatic review settings August 17, 2026 05:23

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.

Review details

  • Files reviewed: 23/23 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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.

DegreesToRadians and RadiansToDegrees overflow and inaccuracy

3 participants