diff --git a/src/libraries/System.Private.CoreLib/src/System/Double.cs b/src/libraries/System.Private.CoreLib/src/System/Double.cs index d4ac8530645ee1..6ea753ba60eadb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Double.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Double.cs @@ -8,6 +8,8 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; using System.Runtime.Versioning; namespace System @@ -1972,22 +1974,242 @@ public static double CosPi(double x) return result; } + // `pi / 180` and `180 / pi` are not exactly representable, so each is carried as a + // `head + mid + tail` triple giving about 159 significant bits, where each term is the + // correctly rounded remainder left by the ones above it. + // + // Two limbs are not always enough. Their accumulated error is about `2^-53` ulp of the + // result, while enumerating every significand whose exact product lands nearest a rounding + // midpoint gives worst cases only `2^-55.58` ulp away for degrees-to-radians and `2^-55.99` + // ulp for the reverse. Those sit well inside that error, so two limbs cannot decide them. + // The third limb takes the error to about `2^-106` ulp, which clears the worst case by 50 + // bits. + internal const double DegreesToRadiansHead = 0.017453292519943295; // 0x1.1DF46A2529D39p-6 + internal const double DegreesToRadiansMid = 2.9486522708701687E-19; // 0x1.5C1D8BECDD291p-62 + internal const double DegreesToRadiansTail = -1.3427726813345382E-35; // -0x1.1D937FA428858p-116 + internal const double RadiansToDegreesHead = 57.29577951308232; // 0x1.CA5DC1A63C1F8p+5 + internal const double RadiansToDegreesMid = -1.9878495670576283E-15; // -0x1.1E7AB456405F9p-49 + internal const double RadiansToDegreesTail = -1.6833394980391744E-31; // -0x1.B505196FABB41p-103 + + // Recovering the roundoff of `value * mid` is what both the two limb and the three limb form + // rest on, and below these that roundoff falls onto the subnormal grid, losing up to one + // smallest subnormal. That loss has to stay under the closest an input brings the exact + // product to a rounding boundary, which is the `2^-55.58` and `2^-55.99` ulp above. Writing + // the smallest subnormal as `2^(emin - p + 1)` and an ulp of the result as + // `2^(e + e_head - p + 1)`, the `p` cancels and the floor on the input exponent `e` is just + // `emin - e_head + w`, giving `-960.42` and `-971.01`. + internal const double DegreesToRadiansMin = 1.0261342003245941E-289; // 0x1p-960 + internal const double RadiansToDegreesMin = 5.010420900022432E-293; // 0x1p-971 + + // At or above these `value * head` overflows, which the triple cannot recover from since the + // roundoff of an infinite product is `inf - inf`. Paired with the minimums above they bound + // the domain MultiplyWide covers, which is what the vector paths range test on. `pi / 180` + // is under one, so nothing finite overflows there and the bound only excludes the non-finite; + // the single unsigned range test covers that the same as any other value. + internal const double DegreesToRadiansMax = PositiveInfinity; + internal const double RadiansToDegreesMax = 3.137566414384587E+306; // 0x1.1DF46A2529D39p+1018 + + // The cold path lifts `value` by `ConversionScaleUp` so the whole computation runs as it + // would in the normal range. Scaling by a power of two is exact in both directions, so any + // exponent in the valid window gives bit-identical results; only the window itself matters. + // + // The floor is the larger of two conditions, evaluated for the smallest input `2^(emin-p+1)` + // and taken over both directions, where `k` is the scale exponent being solved for, + // `p` is the precision in bits including the implicit leading bit, `emin` is the minimum + // normal exponent, and `e_head`/`e_mid` are the constants' own exponents. The `p - 1` term + // is the gap from the smallest subnormal to the smallest normal, so it is the lift needed to + // bring any input into the normal range: + // + // k >= (p - 1) - e_mid so `scaled * mid` stays normal + // k >= 2 * (p - 1) - e_head so the smallest Veltkamp sub-product in the no-FMA + // MultiplyRoundoff stays normal, since it is `2^-(p-1)` + // relative to `scaled * head` + // + // For `double` those give 114 and 110 for degrees-to-radians, 101 and 99 for the reverse, + // so the floor is `2^114`. The ceiling is `2^1022`, keeping `ConversionScaleDown` normal. + // + // `tail` gets no condition. Losing it outright costs `2^-110` relative, and since a result + // is under `2^p` ulp that is under `2^-57` ulp, which the worst case above already clears. + // + // Both conditions are sufficient rather than tight, since an input small enough to violate + // either produces a result with too few significand bits for what is lost to reach the + // rounding position. + private const double ConversionScaleUp = 2.076918743413931E+34; // 0x1p+114 + private const double ConversionScaleDown = 4.81482486096809E-35; // 0x1p-114 + + // Keeps the top 26 significand bits, which is the Veltkamp split MultiplyRoundoff uses. + internal const ulong ConversionSplitMask = 0xFFFF_FFFF_F800_0000; + + // Bounds the error of the two limb form relative to `value * head`, which is what lets that + // form tell whether it resolved the rounding. Three things are dropped or rounded away + // there, none of them reaching `2^-105`: + // + // `value * (C - head - mid)`, which `tail` puts under `2^-108` + // the roundoff of `value * mid`, so `2^-53` of a term already under `2^-54` + // the roundoff of their sum, so `2^-53` of a sum already under `2^-52` + // + // `2^-100` clears the total by more than 16x, leaving room for rounding the ends of the + // interval it forms as well. A wider bound stays correct and only sends more inputs to the + // three limb form; at this width that is about one in `2^46`. + internal const double ConversionErrorScale = 7.888609052210118E-31; // 0x1p-100 + + // The roundoff of `value * head` is itself exactly representable, and recovering it is + // what lets each limb be spent without losing what fell off the end. A fused multiply-add + // gives it directly; otherwise splitting both operands into 26 and 27 bit halves makes + // every sub-product exact, which recovers the same value using only multiplies and adds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double MultiplyRoundoff(double value, double head, double product) + { + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + return FusedMultiplyAdd(value, head, -product); + } + + double headHi = BitConverter.UInt64BitsToDouble(BitConverter.DoubleToUInt64Bits(head) & ConversionSplitMask); + double headLo = head - headHi; + + double valueHi = BitConverter.UInt64BitsToDouble(BitConverter.DoubleToUInt64Bits(value) & ConversionSplitMask); + double valueLo = value - valueHi; + + return (((((valueHi * headHi) - product) + (valueHi * headLo)) + (valueLo * headHi)) + (valueLo * headLo)); + } + + // Computes `value * (head + mid + tail)` as the unevaluated pair `result + residual`, where + // `result` is the correctly rounded product. `value * head` must be known finite and + // non-zero, which is what lets `head` be the nearest representable value rather than being + // biased to keep a sign. + // + // The three limb products are accumulated exactly apart from the two terms already below + // `2^-106` relative, then folded back onto `product` in decreasing order of magnitude. The + // final fold rounds twice, so the correction is first rounded to odd, which is the standard + // way of making the second rounding agree with rounding the exact value once: the odd + // neighbour is never a midpoint, so nothing that decides the outcome can be discarded. + [MethodImpl(MethodImplOptions.NoInlining)] + private static double MultiplyWide(double value, double head, double mid, double tail, out double residual) + { + double product = value * head; + double productError = MultiplyRoundoff(value, head, product); // exact + double middle = value * mid; + double middleError = MultiplyRoundoff(value, mid, middle); // exact but for the + // subnormal loss the + // minimums bound + double bottom = middleError + (value * tail); + + // The magnitudes of `productError` and `middle` are not ordered, so the sum needs the + // form that does not assume it + double sum = productError + middle; + double bias = sum - productError; + double sumError = (productError - (sum - bias)) + (middle - bias); + double lower = sumError + bottom; + + double upper = product + sum; + double upperError = sum - (upper - product); // exact, |product| >= |sum| + + double correction = upperError + lower; + double lost = (upperError - correction) + lower; // exact + + if ((lost != 0) && ((BitConverter.DoubleToUInt64Bits(correction) & 1) == 0)) + { + // Consecutive values alternate in significand parity, so exactly one of the two + // bracketing values is odd and it lies in the direction of what was lost + correction = (lost > 0) ? BitIncrement(correction) : BitDecrement(correction); + } + + double result = upper + correction; + residual = (upper - result) + correction; // exact + return result; + } + + // Computes `value * (head + mid + tail)` as a single rounding, over the domain the minimums + // and maximums above bound. Two limbs are enough for all but the rare input, and whether + // they were is itself decidable: their sum is known to within `bound`, so if both ends of + // that interval round to the same value then so does the exact product. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double MultiplyWide(double value, double head, double mid, double tail) + { + double product = value * head; + + if (!IsFinite(product) || (product == 0)) + { + // The roundoff of an infinite product is `inf - inf`, and a zero product would + // come back from the sum having lost its sign + return product; + } + + double sum = MultiplyRoundoff(value, head, product) + (value * mid); + double bound = Abs(product) * ConversionErrorScale; + double result = product + (sum - bound); + + if (result == (product + (sum + bound))) + { + return result; + } + + return MultiplyWide(value, head, mid, tail, out _); + } + + // Computes `value * (head + mid + tail)` for the inputs too small for MultiplyWide, namely + // those below the minimums above. Scaling up moves the product back into the range where + // the triple is resolved exactly; scaling back down then rounds a second time, with + // `residual` breaking any tie that rounding would otherwise resolve to even. + [MethodImpl(MethodImplOptions.NoInlining)] + private static double ScaleAndMultiplyWide(double value, double head, double mid, double tail) + { + double scaled = value * ConversionScaleUp; + + if (scaled == 0) + { + // Multiplying keeps the sign of zero, which the sum would lose + return scaled * head; + } + + double sum = MultiplyWide(scaled, head, mid, tail, out double residual); + double result = sum * ConversionScaleDown; + + if (residual == 0) + { + return result; + } + + double dropped = sum - (result * ConversionScaleUp); // exact + + if (dropped == 0) + { + // Scaling back down lost nothing, so it was already the single rounding + return result; + } + + double next = (dropped > 0) ? BitIncrement(result) : BitDecrement(result); + + if ((Abs(next - result) * ConversionScaleUp) != (2.0 * Abs(dropped))) + { + return result; + } + + // `sum` sat exactly between `result` and `next`, so the true value did not + return ((residual > 0) == (dropped > 0)) ? next : result; + } + /// public static double DegreesToRadians(double degrees) { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen + if (Abs(degrees) < DegreesToRadiansMin) + { + return ScaleAndMultiplyWide(degrees, DegreesToRadiansHead, DegreesToRadiansMid, DegreesToRadiansTail); + } - return (degrees * Pi) / 180.0; + return MultiplyWide(degrees, DegreesToRadiansHead, DegreesToRadiansMid, DegreesToRadiansTail); } /// public static double RadiansToDegrees(double radians) { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen + if (Abs(radians) < RadiansToDegreesMin) + { + return ScaleAndMultiplyWide(radians, RadiansToDegreesHead, RadiansToDegreesMid, RadiansToDegreesTail); + } - return (radians * 180.0) / Pi; + return MultiplyWide(radians, RadiansToDegreesHead, RadiansToDegreesMid, RadiansToDegreesTail); } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs index 37fdc90a016d46..6ec122ca0b9a31 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Half.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -2324,22 +2324,10 @@ public static bool TryParsePartial(ReadOnlySpan utf8Text, NumberStyles sty public static Half CosPi(Half x) => (Half)float.CosPi((float)x); /// - public static Half DegreesToRadians(Half degrees) - { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen - - return (Half)float.DegreesToRadians((float)degrees); - } + public static Half DegreesToRadians(Half degrees) => (Half)float.DegreesToRadians((float)degrees); /// - public static Half RadiansToDegrees(Half radians) - { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen - - return (Half)float.RadiansToDegrees((float)radians); - } + public static Half RadiansToDegrees(Half radians) => (Half)float.RadiansToDegrees((float)radians); /// public static Half Sin(Half x) => (Half)MathF.Sin((float)x); diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs b/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs index 033777187b62fc..eaa08464a24a67 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs @@ -1188,6 +1188,64 @@ internal static TValue MultiplyDecimalIeee754(TValue left, TVa return NumberToDecimalIeee754BitsFromWide(resultSign, productHigh, productLow, productExponent, sticky: false); } + /// + /// Multiplies the IEEE 754 decimal value represented by by the positive constant + /// (( × 10^Precision) + ) × + /// 10^, rounds the exact product once (round-to-nearest, + /// ties-to-even), and returns its bit pattern. + /// + /// + /// Splitting the constant at the format precision is what keeps this within the double-width machinery + /// the arithmetic operators already use: both halves fit a single limb, so each partial product is one + /// , and the low product is exactly Precision digits below the + /// high one, so shifting it into place is a digit drop whose remainder can only ever be sticky. + /// + /// How many digits a constant needs is a property of that constant, not of the format: for an irrational + /// constant the closest a Precision digit significand drives the exact product to a midpoint has + /// no closed form, so it has to be enumerated against the truncation error once per constant rather than + /// carried over from a narrower format. See Decimal128 for the existing pair. + /// + internal static TValue MultiplyByWideConstantDecimalIeee754(TValue bits, TValue constantHead, TValue constantTail, int constantExponent) + where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo + where TValue : unmanaged, IBinaryInteger + { + if (TDecimal.IsNaN(bits)) + { + return PropagateNaN(bits, bits); + } + + if (TDecimal.IsInfinity(bits)) + { + // The constant is positive and finite, so the infinity carries through with its sign + return TDecimal.IsNegative(bits) ? TDecimal.NegativeInfinity : TDecimal.PositiveInfinity; + } + + DecodedDecimalIeee754 value = UnpackDecimalIeee754(bits); + + int precision = TDecimal.Precision; + + if (TValue.IsZero(value.Significand)) + { + // The IEEE 754 preferred exponent for a product is the sum of the operand exponents, and the + // constant's own quantum is 10^constantExponent + return DecimalIeee754FiniteNumberBinaryEncoding(value.Signed, TValue.Zero, value.UnbiasedExponent + constantExponent); + } + + // `significand × constantHead` spans at most `2p + 2` digits and `significand × constantTail` at most + // `2p`, so both fit the limb pair, and everything the alignment drop removes is far enough below the + // result's last digit to only ever reach it through `sticky`. + WideMultiply(value.Significand, constantHead, out TValue high, out TValue low); + WideMultiply(value.Significand, constantTail, out TValue tailHigh, out TValue tailLow); + + bool sticky = false; + DropDigits(ref tailHigh, ref tailLow, precision, ref sticky, out int roundDigit); + sticky |= roundDigit != 0; + + WideAdd(high, low, tailHigh, tailLow, out high, out low); + + return NumberToDecimalIeee754BitsFromWide(value.Signed, high, low, value.UnbiasedExponent + constantExponent + precision, sticky); + } + /// /// Computes (left × right) + addend for three IEEE 754 decimal values represented by their raw bit /// patterns, rounds the exact result once (round-to-nearest, ties-to-even), and returns its bit pattern. @@ -2467,14 +2525,20 @@ private static void WideScaleByPow10(ref TValue high, ref TVal private static void WideDropLowDigits(ref TValue high, ref TValue low, int dropCount, ref bool sticky) where TValue : unmanaged, IBinaryInteger { - for (int i = 0; i < dropCount; i++) + int chunkPower = SinglePassPow10(out TValue chunkDivisor); + + while (dropCount > 0) { if (TValue.IsZero(high) && TValue.IsZero(low)) { break; } - sticky |= WideDivideByTen(ref high, ref low) != 0; + int step = Math.Min(dropCount, chunkPower); + TValue divisor = (step == chunkPower) ? chunkDivisor : WidePow10(step); + + sticky |= !TValue.IsZero(WideDivideByPow10(ref high, ref low, divisor)); + dropCount -= step; } } @@ -2498,6 +2562,23 @@ private static void AlignWideToCommonExponent(ref TValue high, } } + /// + /// Adds the double-width magnitudes (, ) and + /// (, ), which the caller guarantees cannot carry + /// out of the high limb. + /// + private static void WideAdd(TValue leftHigh, TValue leftLow, TValue rightHigh, TValue rightLow, out TValue high, out TValue low) + where TValue : unmanaged, IBinaryInteger + { + low = leftLow + rightLow; + high = leftHigh + rightHigh; + + if (low < leftLow) + { + high += TValue.One; + } + } + /// /// Compares two double-width magnitudes, returning a negative value, zero, or a positive value according to /// whether the first is less than, equal to, or greater than the second. @@ -2527,27 +2608,71 @@ private static void WideSubtract(TValue leftHigh, TValue leftLow, TValue } /// - /// Divides the double-width value in (, ) by ten in place, - /// returning the discarded least-significant decimal digit. Used to strip low-order digits during rounding. + /// Gets the largest power of ten that a single pass can strip, + /// returning its exponent and, in , its value. /// /// - /// Only the 128-bit format reaches this helper: the 32-bit and 64-bit formats widen the limb pair to a - /// single native integer and divide directly (see and - /// ). The Intel reference implementation avoids hardware - /// division here by multiplying with precomputed reciprocals of powers of ten (e.g. - /// bid_reciprocals10_64) and shifting; this helper instead uses direct integer division for - /// simplicity, and adopting the reciprocal-multiply tables is a possible future performance optimization. + /// The bound is 10^k <= 2^(bits / 2), which is what keeps the running remainder times the + /// half-width base inside : 10^4 for a 32-bit limb, 10^9 for a + /// 64-bit limb, and 10^19 for a 128-bit limb. /// - private static int WideDivideByTen(ref TValue high, ref TValue low) + private static int SinglePassPow10(out TValue divisor) + where TValue : unmanaged, IBinaryInteger + { + TValue ten = TValue.CreateTruncating(10); + TValue limit = (TValue.One << ((TValue.Zero.GetByteCount() * 8) / 2)) / ten; + + divisor = TValue.One; + int power = 0; + + while (divisor <= limit) + { + divisor *= ten; + power++; + } + + return power; + } + + /// + /// Computes 10^, which the caller guarantees fits a single limb. + /// + private static TValue WidePow10(int power) where TValue : unmanaged, IBinaryInteger { TValue ten = TValue.CreateTruncating(10); + TValue result = TValue.One; + for (int i = 0; i < power; i++) + { + result *= ten; + } + + return result; + } + + /// + /// Divides the double-width value in (, ) by + /// in place, returning the discarded low-order digits. Used to strip low-order + /// digits during rounding, a whole chunk of them at a time. + /// + /// + /// The rounding paths only reach this helper with a two-limb value for the 128-bit format: the 32-bit and + /// 64-bit formats widen the limb pair to a single native integer and divide directly (see + /// and ). Addition + /// reaches it for every format through . The Intel reference + /// implementation avoids hardware division here by multiplying with precomputed reciprocals of powers of ten + /// (e.g. bid_reciprocals10_64) and shifting; this helper instead uses direct integer division for + /// simplicity, and adopting the reciprocal-multiply tables is a possible future performance optimization. + /// + private static TValue WideDivideByPow10(ref TValue high, ref TValue low, TValue divisor) + where TValue : unmanaged, IBinaryInteger + { if (TValue.IsZero(high)) { - (TValue quotient, TValue remainder) = TValue.DivRem(low, ten); + (TValue quotient, TValue remainder) = TValue.DivRem(low, divisor); low = quotient; - return int.CreateTruncating(remainder); + return remainder; } int bits = TValue.Zero.GetByteCount() * 8; @@ -2555,17 +2680,18 @@ private static int WideDivideByTen(ref TValue high, ref TValue low) TValue lowMask = (TValue.One << half) - TValue.One; TValue baseValue = TValue.One << half; - // Long division of the four half-width limbs (most significant first) by ten. Because the running - // remainder stays below ten, `remainder * baseValue + limb` never exceeds the integer width. + // Long division of the four half-width limbs (most significant first). Because the running remainder + // stays below the divisor, which `SinglePassPow10` caps at `baseValue`, the intermediate + // `remainder * baseValue + limb` never exceeds the integer width. TValue rem = TValue.Zero; - (TValue q3, rem) = TValue.DivRem((rem * baseValue) + (high >> half), ten); - (TValue q2, rem) = TValue.DivRem((rem * baseValue) + (high & lowMask), ten); - (TValue q1, rem) = TValue.DivRem((rem * baseValue) + (low >> half), ten); - (TValue q0, rem) = TValue.DivRem((rem * baseValue) + (low & lowMask), ten); + (TValue q3, rem) = TValue.DivRem((rem * baseValue) + (high >> half), divisor); + (TValue q2, rem) = TValue.DivRem((rem * baseValue) + (high & lowMask), divisor); + (TValue q1, rem) = TValue.DivRem((rem * baseValue) + (low >> half), divisor); + (TValue q0, rem) = TValue.DivRem((rem * baseValue) + (low & lowMask), divisor); high = (q3 << half) | q2; low = (q1 << half) | q0; - return int.CreateTruncating(rem); + return rem; } /// @@ -2599,11 +2725,14 @@ private static int WideDigitCount(TValue high, TValue low) } int count = 0; + int chunkPower = SinglePassPow10(out TValue chunkDivisor); + // A non-zero high limb means the value spans at least the full limb width, which is more digits than a + // single pass strips, so the count stays additive across each pass. while (!TValue.IsZero(high)) { - WideDivideByTen(ref high, ref low); - count++; + WideDivideByPow10(ref high, ref low, chunkDivisor); + count += chunkPower; } return count + TDecimal.CountDigits(low); @@ -2735,16 +2864,22 @@ private static TValue DropDigits(ref TValue high, ref TValue l return low; } - for (int i = 0; i < dropCount; i++) + // Every removed digit below the most-significant one only contributes stickiness, so they come off a + // whole single-pass chunk at a time; the rounding digit is then stripped on its own. + int chunkPower = SinglePassPow10(out TValue chunkDivisor); + int remaining = dropCount - 1; + + while (remaining > 0) { - if (i > 0) - { - sticky |= roundDigit != 0; - } + int step = Math.Min(remaining, chunkPower); + TValue divisor = (step == chunkPower) ? chunkDivisor : WidePow10(step); - roundDigit = WideDivideByTen(ref high, ref low); + sticky |= !TValue.IsZero(WideDivideByPow10(ref high, ref low, divisor)); + remaining -= step; } + roundDigit = int.CreateTruncating(WideDivideByPow10(ref high, ref low, TValue.CreateTruncating(10))); + Debug.Assert(TValue.IsZero(high)); return low; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs index d97bda482c571a..ae6b65813be1f8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs @@ -2045,22 +2045,10 @@ public static bool TryParsePartial(ReadOnlySpan utf8Text, NumberStyles sty public static BFloat16 CosPi(BFloat16 x) => (BFloat16)float.CosPi((float)x); /// - public static BFloat16 DegreesToRadians(BFloat16 degrees) - { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen - - return (BFloat16)float.DegreesToRadians((float)degrees); - } + public static BFloat16 DegreesToRadians(BFloat16 degrees) => (BFloat16)float.DegreesToRadians((float)degrees); /// - public static BFloat16 RadiansToDegrees(BFloat16 radians) - { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen - - return (BFloat16)float.RadiansToDegrees((float)radians); - } + public static BFloat16 RadiansToDegrees(BFloat16 radians) => (BFloat16)float.RadiansToDegrees((float)radians); /// public static BFloat16 Sin(BFloat16 x) => (BFloat16)float.Sin((float)x); diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs index b91149c6148b97..e3c27363a55b0d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs @@ -61,6 +61,25 @@ public readonly struct Decimal128 private const ulong SNaNMaskUpper = 0x7E00_0000_0000_0000; private const ulong InfinityMaskUpper = 0x7800_0000_0000_0000; + // `pi / 180` and `180 / pi` cannot be written exactly in decimal, so each is kept as two + // integers and an exponent, `(Head * 10^34 + Tail) * 10^Exponent`. That is just the digits of + // the value with the point moved: `pi / 180 = 1.745_329...e-2`, `180 / pi = 5.729_577...e+1`, + // and since the pair is a `2p + 2` digit integer, `Exponent` is `-(2p + 1)` plus that `-2` or `+1`. + // + // `Tail` is `p` digits and `Head` is `p + 2`, since `Head` is bounded by the `UInt128` it is + // stored in rather than by the format. Those two extra digits are the accuracy: the product + // is exact and rounded only once, so the digits dropped off the constant are the only error, + // and they have to stay below the closest a `p` digit input can push the product to a + // rounding tie. Every format stops at two, `Decimal32` being the one with no room for more, + // since `180 / pi` at `p + 3` digits no longer fits its `uint`. + private static UInt128 DegreesToRadiansHead => new UInt128(0x0021_9D23_38C9_2382, 0xBF86_F9E5_ACDE_A057); // 174_532_925_199_432_957_692_369_076_848_861_271 + private static UInt128 DegreesToRadiansTail => new UInt128(0x0000_A9BF_270D_B49E, 0x82D0_06FB_7D2C_4001); // 3_442_871_888_541_725_456_097_191_440_171_009 + private const int DegreesToRadiansExponent = -71; + + private static UInt128 RadiansToDegreesHead => new UInt128(0x006E_5900_1427_58B2, 0xDB41_A116_5AA0_7F37); // 572_957_795_130_823_208_767_981_548_141_051_703 + private static UInt128 RadiansToDegreesTail => new UInt128(0x0000_9FC5_74E0_7F0D, 0x2D46_04B9_0C4C_EA5D); // 3_240_547_246_656_432_154_916_024_386_120_285 + private const int RadiansToDegreesExponent = -68; + /// Gets a value that represents positive infinity. public static Decimal128 PositiveInfinity => new Decimal128(PositiveInfinityValue); @@ -954,6 +973,9 @@ bool IFloatingPoint.TryWriteSignificandLittleEndian(Span desti /// public static Decimal128 Cosh(Decimal128 x) => new Decimal128(Number.CoshDecimalIeee754(new UInt128(x._upper, x._lower))); + /// + public static Decimal128 DegreesToRadians(Decimal128 degrees) => new Decimal128(Number.MultiplyByWideConstantDecimalIeee754(new UInt128(degrees._upper, degrees._lower), DegreesToRadiansHead, DegreesToRadiansTail, DegreesToRadiansExponent)); + /// public static Decimal128 Exp(Decimal128 x) => new Decimal128(Number.ExpDecimalIeee754(new UInt128(x._upper, x._lower))); @@ -1011,6 +1033,9 @@ bool IFloatingPoint.TryWriteSignificandLittleEndian(Span desti /// public static Decimal128 Pow(Decimal128 x, Decimal128 y) => new Decimal128(Number.PowDecimalIeee754(new UInt128(x._upper, x._lower), new UInt128(y._upper, y._lower))); + /// + public static Decimal128 RadiansToDegrees(Decimal128 radians) => new Decimal128(Number.MultiplyByWideConstantDecimalIeee754(new UInt128(radians._upper, radians._lower), RadiansToDegreesHead, RadiansToDegreesTail, RadiansToDegreesExponent)); + /// public static Decimal128 ReciprocalEstimate(Decimal128 x) => One / x; diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs index b46866b0525ac3..3f165d8ade6857 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs @@ -64,6 +64,15 @@ internal Decimal32(uint value) private const uint MaxInternalValue = 0x77F8_967F; // +9.999_999 * 10^96; aka +9_999_999 * 10^90 private const uint MinInternalValue = 0xF7F8_967F; // -9.999_999 * 10^96; aka -9_999_999 * 10^90 + // See `Decimal128` for how these are laid out and why they carry the digits they do. + private const uint DegreesToRadiansHead = 174_532_925; + private const uint DegreesToRadiansTail = 1_994_330; + private const int DegreesToRadiansExponent = -17; + + private const uint RadiansToDegreesHead = 572_957_795; + private const uint RadiansToDegreesTail = 1_308_232; + private const int RadiansToDegreesExponent = -14; + /// Gets a value that represents positive infinity. public static Decimal32 PositiveInfinity => new Decimal32(PositiveInfinityValue); @@ -975,6 +984,9 @@ bool IFloatingPoint.TryWriteSignificandLittleEndian(Span destin /// public static Decimal32 Cosh(Decimal32 x) => new Decimal32(Number.CoshDecimalIeee754(x._value)); + /// + public static Decimal32 DegreesToRadians(Decimal32 degrees) => new Decimal32(Number.MultiplyByWideConstantDecimalIeee754(degrees._value, DegreesToRadiansHead, DegreesToRadiansTail, DegreesToRadiansExponent)); + /// public static Decimal32 Exp(Decimal32 x) => new Decimal32(Number.ExpDecimalIeee754(x._value)); @@ -1032,6 +1044,9 @@ bool IFloatingPoint.TryWriteSignificandLittleEndian(Span destin /// public static Decimal32 Pow(Decimal32 x, Decimal32 y) => new Decimal32(Number.PowDecimalIeee754(x._value, y._value)); + /// + public static Decimal32 RadiansToDegrees(Decimal32 radians) => new Decimal32(Number.MultiplyByWideConstantDecimalIeee754(radians._value, RadiansToDegreesHead, RadiansToDegreesTail, RadiansToDegreesExponent)); + /// public static Decimal32 ReciprocalEstimate(Decimal32 x) => One / x; diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs index 56fde15e2c7d9c..fbc675d8d688bd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs @@ -59,6 +59,15 @@ public readonly struct Decimal64 private const ulong MaxInternalValue = 0x77FB_86F2_6FC0_FFFF; // 9.999_999_999_999_999 * 10^384; aka 9_999_999_999_999_999 * 10^369 private const ulong MinInternalValue = 0xF7FB_86F2_6FC0_FFFF; // -9.999_999_999_999_999 * 10^384; aka -9_999_999_999_999_999 * 10^369 + // See `Decimal128` for how these are laid out and why they carry the digits they do. + private const ulong DegreesToRadiansHead = 174_532_925_199_432_957; + private const ulong DegreesToRadiansTail = 6_923_690_768_488_613; + private const int DegreesToRadiansExponent = -35; + + private const ulong RadiansToDegreesHead = 572_957_795_130_823_208; + private const ulong RadiansToDegreesTail = 7_679_815_481_410_517; + private const int RadiansToDegreesExponent = -32; + /// Gets a value that represents positive infinity. public static Decimal64 PositiveInfinity => new Decimal64(PositiveInfinityValue); @@ -968,6 +977,9 @@ bool IFloatingPoint.TryWriteSignificandLittleEndian(Span destin /// public static Decimal64 Cosh(Decimal64 x) => new Decimal64(Number.CoshDecimalIeee754(x._value)); + /// + public static Decimal64 DegreesToRadians(Decimal64 degrees) => new Decimal64(Number.MultiplyByWideConstantDecimalIeee754(degrees._value, DegreesToRadiansHead, DegreesToRadiansTail, DegreesToRadiansExponent)); + /// public static Decimal64 Exp(Decimal64 x) => new Decimal64(Number.ExpDecimalIeee754(x._value)); @@ -1025,6 +1037,9 @@ bool IFloatingPoint.TryWriteSignificandLittleEndian(Span destin /// public static Decimal64 Pow(Decimal64 x, Decimal64 y) => new Decimal64(Number.PowDecimalIeee754(x._value, y._value)); + /// + public static Decimal64 RadiansToDegrees(Decimal64 radians) => new Decimal64(Number.MultiplyByWideConstantDecimalIeee754(radians._value, RadiansToDegreesHead, RadiansToDegreesTail, RadiansToDegreesExponent)); + /// public static Decimal64 ReciprocalEstimate(Decimal64 x) => One / x; diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs index eedb7952551548..7a1a1f7926fc5b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/ITrigonometricFunctions.cs @@ -109,6 +109,17 @@ static virtual TSelf DegreesToRadians(TSelf degrees) // This is particularly true for `RadiansToDegrees` where 180 is exactly representable // and so allows an exactly representable intermediate value to be computed when overflow // doesn't occur. + // + // Which of the two forms wins is decided by where the nearest representable `pi / 180` + // falls relative to the infinitely precise value, which lands more or less uniformly for + // each number of significand bits. Folding therefore ranges from far worse to far better + // depending on the type, while this form stays between 55% and 75% correctly rounded + // everywhere. A DIM cannot pick the better half of that, since it can only reach + // `TSelf.Pi` and so must round twice to get there. + // + // A concrete type has neither constraint. It can name the constant directly and knows + // what its hardware and any wider type give it, so it can be correctly rounded over the + // whole domain, which every type here does and any other implementation is free to do. return (degrees * TSelf.Pi) / TSelf.CreateChecked(180); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector.cs index 636e38112b3169..6dad48f30f9526 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Vector.cs @@ -1180,7 +1180,7 @@ public static Vector DegreesToRadians(Vector degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, double>(degrees); + return VectorMath.DegreesToRadiansDouble, Vector>(degrees); } else { @@ -1195,7 +1195,7 @@ public static Vector DegreesToRadians(Vector degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, float>(degrees); + return VectorMath.DegreesToRadiansSingle, Vector>(degrees); } else { @@ -2848,7 +2848,7 @@ public static Vector RadiansToDegrees(Vector radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, double>(radians); + return VectorMath.RadiansToDegreesDouble, Vector>(radians); } else { @@ -2862,7 +2862,7 @@ public static Vector RadiansToDegrees(Vector radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, float>(radians); + return VectorMath.RadiansToDegreesSingle, Vector>(radians); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs index 829430f245d40f..bdce7f65bb07ac 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector128.cs @@ -1736,7 +1736,7 @@ public static Vector128 DegreesToRadians(Vector128 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, double>(degrees); + return VectorMath.DegreesToRadiansDouble, Vector128>(degrees); } else { @@ -1754,7 +1754,14 @@ public static Vector128 DegreesToRadians(Vector128 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, float>(degrees); + if (Vector256.IsHardwareAccelerated) + { + return VectorMath.DegreesToRadiansSingle, Vector256>(degrees); + } + else + { + return VectorMath.DegreesToRadiansSingle, Vector128>(degrees); + } } else { @@ -3110,7 +3117,7 @@ public static Vector128 RadiansToDegrees(Vector128 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, double>(radians); + return VectorMath.RadiansToDegreesDouble, Vector128>(radians); } else { @@ -3128,7 +3135,14 @@ public static Vector128 RadiansToDegrees(Vector128 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, float>(radians); + if (Vector256.IsHardwareAccelerated) + { + return VectorMath.RadiansToDegreesSingle, Vector256>(radians); + } + else + { + return VectorMath.RadiansToDegreesSingle, Vector128>(radians); + } } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector256.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector256.cs index 9134f25f408861..171f95fa258dc4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector256.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector256.cs @@ -1830,7 +1830,7 @@ public static Vector256 DegreesToRadians(Vector256 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, double>(degrees); + return VectorMath.DegreesToRadiansDouble, Vector256>(degrees); } else { @@ -1848,7 +1848,14 @@ public static Vector256 DegreesToRadians(Vector256 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, float>(degrees); + if (Vector512.IsHardwareAccelerated) + { + return VectorMath.DegreesToRadiansSingle, Vector512>(degrees); + } + else + { + return VectorMath.DegreesToRadiansSingle, Vector256>(degrees); + } } else { @@ -3153,7 +3160,7 @@ public static Vector256 RadiansToDegrees(Vector256 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, double>(radians); + return VectorMath.RadiansToDegreesDouble, Vector256>(radians); } else { @@ -3171,7 +3178,14 @@ public static Vector256 RadiansToDegrees(Vector256 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, float>(radians); + if (Vector512.IsHardwareAccelerated) + { + return VectorMath.RadiansToDegreesSingle, Vector512>(radians); + } + else + { + return VectorMath.RadiansToDegreesSingle, Vector256>(radians); + } } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector512.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector512.cs index 5ab9fb26a39d43..ccf5ffd1e9c8bd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector512.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector512.cs @@ -1885,7 +1885,7 @@ public static Vector512 DegreesToRadians(Vector512 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, double>(degrees); + return VectorMath.DegreesToRadiansDouble, Vector512>(degrees); } else { @@ -1903,7 +1903,7 @@ public static Vector512 DegreesToRadians(Vector512 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, float>(degrees); + return VectorMath.DegreesToRadiansSingle, Vector512>(degrees); } else { @@ -3194,7 +3194,7 @@ public static Vector512 RadiansToDegrees(Vector512 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, double>(radians); + return VectorMath.RadiansToDegreesDouble, Vector512>(radians); } else { @@ -3212,7 +3212,7 @@ public static Vector512 RadiansToDegrees(Vector512 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, float>(radians); + return VectorMath.RadiansToDegreesSingle, Vector512>(radians); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector64.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector64.cs index 3639b3ae6551e6..f4b04d311cfc29 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/Vector64.cs @@ -1679,7 +1679,7 @@ public static Vector64 DegreesToRadians(Vector64 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, double>(degrees); + return VectorMath.DegreesToRadiansDouble, Vector64>(degrees); } else { @@ -1696,7 +1696,14 @@ public static Vector64 DegreesToRadians(Vector64 degrees) { if (IsHardwareAccelerated) { - return VectorMath.DegreesToRadians, float>(degrees); + if (Vector128.IsHardwareAccelerated) + { + return VectorMath.DegreesToRadiansSingle, Vector128>(degrees); + } + else + { + return VectorMath.DegreesToRadiansSingle, Vector64>(degrees); + } } else { @@ -3277,7 +3284,7 @@ public static Vector64 RadiansToDegrees(Vector64 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, double>(radians); + return VectorMath.RadiansToDegreesDouble, Vector64>(radians); } else { @@ -3294,7 +3301,14 @@ public static Vector64 RadiansToDegrees(Vector64 radians) { if (IsHardwareAccelerated) { - return VectorMath.RadiansToDegrees, float>(radians); + if (Vector128.IsHardwareAccelerated) + { + return VectorMath.RadiansToDegreesSingle, Vector128>(radians); + } + else + { + return VectorMath.RadiansToDegreesSingle, Vector64>(radians); + } } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/VectorMath.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/VectorMath.cs index bdbe668efa326b..510206a9ad3d1c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/VectorMath.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Intrinsics/VectorMath.cs @@ -338,15 +338,126 @@ public static TVector CopySign(TVector value, TVector sign) } } + // Computes `value * (head + mid)`, and reports per element whether that resolved the + // rounding. See `double.MultiplyWide` for the interval this tests. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TVector DegreesToRadians(TVector degrees) - where TVector : unmanaged, ISimdVector - where T : IFloatingPointIeee754 + private static TVectorDouble MultiplyWideDouble(TVectorDouble value, double head, double mid, out TVectorUInt64 resolved) + where TVectorDouble : unmanaged, ISimdVector + where TVectorUInt64 : unmanaged, ISimdVector + { + TVectorDouble vhead = Create(head); + + TVectorDouble product = value * vhead; + TVectorDouble sum = MultiplyRoundoffDouble(value, head, vhead, product) + + (value * Create(mid)); + + TVectorDouble bound = TVectorDouble.Abs(product) * Create(double.ConversionErrorScale); + TVectorDouble result = product + (sum - bound); + + // Handed back as a mask rather than reduced here, so the caller can fold it into the + // range test and branch once, and as a `ulong` mask because reducing the same all ones + // pattern as `double` is several times more expensive for no difference in meaning. + resolved = Unsafe.BitCast(TVectorDouble.Equals(result, product + (sum + bound))); + return result; + } + + // The vector form of `double.MultiplyRoundoff`, and it has to be that rather than + // `MultiplyAddEstimate`: that one is free to evaluate as a separate multiply and add, and + // then it returns `product - product`, silently dropping the term the two limb form is + // built on. `head` is a constant, so only `value` is split at runtime. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TVectorDouble MultiplyRoundoffDouble(TVectorDouble value, double head, TVectorDouble vhead, TVectorDouble product) + where TVectorDouble : unmanaged, ISimdVector + where TVectorUInt64 : unmanaged, ISimdVector + { + if (Fma.IsSupported || AdvSimd.Arm64.IsSupported) + { + return FusedMultiplyAdd(value, vhead, -product); + } + + double headHi = BitConverter.UInt64BitsToDouble(BitConverter.DoubleToUInt64Bits(head) & double.ConversionSplitMask); + + TVectorDouble vheadHi = Create(headHi); + TVectorDouble vheadLo = Create(head - headHi); + + TVectorDouble valueHi = Unsafe.BitCast( + Unsafe.BitCast(value) & TVectorUInt64.Create(double.ConversionSplitMask) + ); + TVectorDouble valueLo = value - valueHi; + + return ((((valueHi * vheadHi) - product) + (valueHi * vheadLo)) + (valueLo * vheadHi)) + (valueLo * vheadLo); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TVectorDouble DegreesToRadiansDouble(TVectorDouble degrees) + where TVectorDouble : unmanaged, ISimdVector + where TVectorUInt64 : unmanaged, ISimdVector + { + // See `double.DegreesToRadians` for why the constant is carried as a `head + mid + tail` + // triple, and `double.DegreesToRadiansMin` and `Max` for the window the two limb form + // used here is valid over. + // + // That window is a range test rather than a pair of compares: bit patterns of + // non-negative values increase with magnitude, so subtracting the low end makes one + // unsigned compare reject both ends at once, zero and the non-finite values included. + // An element outside it, or one the two limb form cannot resolve, sends the whole + // vector to the scalar path rather than growing a third limb across it. Both are + // elementwise masks, so they reduce to one branch together. + + TVectorUInt64 ux = Unsafe.BitCast(TVectorDouble.Abs(degrees)) + - TVectorUInt64.Create(BitConverter.DoubleToUInt64Bits(double.DegreesToRadiansMin)); + + TVectorUInt64 window = TVectorUInt64.Create(BitConverter.DoubleToUInt64Bits(double.DegreesToRadiansMax) + - BitConverter.DoubleToUInt64Bits(double.DegreesToRadiansMin)); + + TVectorDouble result = MultiplyWideDouble( + degrees, + double.DegreesToRadiansHead, + double.DegreesToRadiansMid, + out TVectorUInt64 resolved + ); + + if (TVectorUInt64.EqualsAll(resolved & TVectorUInt64.LessThan(ux, window), TVectorUInt64.AllBitsSet)) + { + return result; + } + + return ScalarFallback(degrees); + + static TVectorDouble ScalarFallback(TVectorDouble x) + { + TVectorDouble result = TVectorDouble.Zero; + + for (int i = 0; i < TVectorDouble.ElementCount; i++) + { + double scalar = double.DegreesToRadians(x[i]); + result = result.WithElement(i, scalar); + } + + return result; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TVectorSingle DegreesToRadiansSingle(TVectorSingle degrees) + where TVectorSingle : unmanaged, ISimdVector + where TVectorDouble : unmanaged, ISimdVector { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen + // See `float.DegreesToRadians` for why widening is correctly rounded here. - return (degrees * TVector.Create(T.Pi)) / TVector.Create(T.CreateTruncating(180)); + TVectorDouble scale = Create(double.DegreesToRadiansHead); + + if (TVectorSingle.ElementCount == TVectorDouble.ElementCount) + { + return Narrow( + Widen(degrees) * scale + ); + } + + return Narrow( + WidenLower(degrees) * scale, + WidenUpper(degrees) * scale + ); } public static TVectorDouble ExpDouble(TVectorDouble x) @@ -1681,14 +1792,66 @@ public static TVector MinNumber(TVector x, TVector y) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TVector RadiansToDegrees(TVector radians) - where TVector : unmanaged, ISimdVector - where T : IFloatingPointIeee754 + public static TVectorDouble RadiansToDegreesDouble(TVectorDouble radians) + where TVectorDouble : unmanaged, ISimdVector + where TVectorUInt64 : unmanaged, ISimdVector { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen + // See `DegreesToRadiansDouble` for the shape of this. + + TVectorUInt64 ux = Unsafe.BitCast(TVectorDouble.Abs(radians)) + - TVectorUInt64.Create(BitConverter.DoubleToUInt64Bits(double.RadiansToDegreesMin)); - return (radians * TVector.Create(T.CreateTruncating(180))) / TVector.Create(T.Pi); + TVectorUInt64 window = TVectorUInt64.Create(BitConverter.DoubleToUInt64Bits(double.RadiansToDegreesMax) + - BitConverter.DoubleToUInt64Bits(double.RadiansToDegreesMin)); + + TVectorDouble result = MultiplyWideDouble( + radians, + double.RadiansToDegreesHead, + double.RadiansToDegreesMid, + out TVectorUInt64 resolved + ); + + if (TVectorUInt64.EqualsAll(resolved & TVectorUInt64.LessThan(ux, window), TVectorUInt64.AllBitsSet)) + { + return result; + } + + return ScalarFallback(radians); + + static TVectorDouble ScalarFallback(TVectorDouble x) + { + TVectorDouble result = TVectorDouble.Zero; + + for (int i = 0; i < TVectorDouble.ElementCount; i++) + { + double scalar = double.RadiansToDegrees(x[i]); + result = result.WithElement(i, scalar); + } + + return result; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TVectorSingle RadiansToDegreesSingle(TVectorSingle radians) + where TVectorSingle : unmanaged, ISimdVector + where TVectorDouble : unmanaged, ISimdVector + { + // See `DegreesToRadiansSingle` for the shape of this. + + TVectorDouble scale = Create(double.RadiansToDegreesHead); + + if (TVectorSingle.ElementCount == TVectorDouble.ElementCount) + { + return Narrow( + Widen(radians) * scale + ); + } + + return Narrow( + WidenLower(radians) * scale, + WidenUpper(radians) * scale + ); } public static TVectorDouble RoundDouble(TVectorDouble vector, MidpointRounding mode) @@ -2511,6 +2674,40 @@ private static TVectorDouble CosSingleSmall(TVectorDouble x) return TVectorDouble.MultiplyAddEstimate(CosSinglePoly(x), x4, s); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TVectorDouble FusedMultiplyAdd(TVectorDouble left, TVectorDouble right, TVectorDouble addend) + where TVectorDouble : unmanaged, ISimdVector + { + Unsafe.SkipInit(out TVectorDouble result); + + if (typeof(TVectorDouble) == typeof(Vector)) + { + result = (TVectorDouble)(object)Vector.FusedMultiplyAdd((Vector)(object)left, (Vector)(object)right, (Vector)(object)addend); + } + else if (typeof(TVectorDouble) == typeof(Vector64)) + { + result = (TVectorDouble)(object)Vector64.FusedMultiplyAdd((Vector64)(object)left, (Vector64)(object)right, (Vector64)(object)addend); + } + else if (typeof(TVectorDouble) == typeof(Vector128)) + { + result = (TVectorDouble)(object)Vector128.FusedMultiplyAdd((Vector128)(object)left, (Vector128)(object)right, (Vector128)(object)addend); + } + else if (typeof(TVectorDouble) == typeof(Vector256)) + { + result = (TVectorDouble)(object)Vector256.FusedMultiplyAdd((Vector256)(object)left, (Vector256)(object)right, (Vector256)(object)addend); + } + else if (typeof(TVectorDouble) == typeof(Vector512)) + { + result = (TVectorDouble)(object)Vector512.FusedMultiplyAdd((Vector512)(object)left, (Vector512)(object)right, (Vector512)(object)addend); + } + else + { + ThrowHelper.ThrowNotSupportedException(); + } + + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TVector Create(double value) where TVector : unmanaged, ISimdVector diff --git a/src/libraries/System.Private.CoreLib/src/System/Single.cs b/src/libraries/System.Private.CoreLib/src/System/Single.cs index 65c893ca9db868..dd29e4daece2be 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Single.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Single.cs @@ -1888,23 +1888,17 @@ public static float CosPi(float x) return result; } - /// - public static float DegreesToRadians(float degrees) - { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen + // Multiplying by `head` alone is enough here, without the rest of the triple that `double` + // needs: the constant being inexact is under `2^-54.6` relative and the single rounding of + // the product is `2^-53`, together under `2^-28.6` of a `float` ulp, while no significand + // brings the exact value nearer than `2^-26.5` ulp to the boundary between two results + // (`2^-26.470` for `DegreesToRadians`, `2^-24.5` for `RadiansToDegrees`). - return (degrees * Pi) / 180.0f; - } + /// + public static float DegreesToRadians(float degrees) => (float)(degrees * double.DegreesToRadiansHead); /// - public static float RadiansToDegrees(float radians) - { - // NOTE: Don't change the algorithm without consulting the DIM - // which elaborates on why this implementation was chosen - - return (radians * 180.0f) / Pi; - } + public static float RadiansToDegrees(float radians) => (float)(radians * double.RadiansToDegreesHead); /// [Intrinsic] diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 1568b3292f8b25..0d29de504ab327 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -11481,6 +11481,7 @@ namespace System.Numerics public static System.Numerics.Decimal128 DecodeBinary(System.UInt128 x) { throw null; } [System.CLSCompliantAttribute(false)] public static System.Numerics.Decimal128 DecodeDecimal(System.UInt128 x) { throw null; } + public static System.Numerics.Decimal128 DegreesToRadians(System.Numerics.Decimal128 degrees) { throw null; } [System.CLSCompliantAttribute(false)] public static System.UInt128 EncodeBinary(System.Numerics.Decimal128 x) { throw null; } [System.CLSCompliantAttribute(false)] @@ -11623,6 +11624,7 @@ namespace System.Numerics public static System.Numerics.Decimal128 Parse(string s, System.IFormatProvider? provider) { throw null; } public static System.Numerics.Decimal128 Pow(System.Numerics.Decimal128 x, System.Numerics.Decimal128 y) { throw null; } public static System.Numerics.Decimal128 Quantize(System.Numerics.Decimal128 x, System.Numerics.Decimal128 y) { throw null; } + public static System.Numerics.Decimal128 RadiansToDegrees(System.Numerics.Decimal128 radians) { throw null; } public static System.Numerics.Decimal128 ReciprocalEstimate(System.Numerics.Decimal128 x) { throw null; } public static System.Numerics.Decimal128 ReciprocalSqrtEstimate(System.Numerics.Decimal128 x) { throw null; } public static System.Numerics.Decimal128 RootN(System.Numerics.Decimal128 x, int n) { throw null; } @@ -11731,6 +11733,7 @@ namespace System.Numerics public static System.Numerics.Decimal32 DecodeBinary(uint x) { throw null; } [System.CLSCompliantAttribute(false)] public static System.Numerics.Decimal32 DecodeDecimal(uint x) { throw null; } + public static System.Numerics.Decimal32 DegreesToRadians(System.Numerics.Decimal32 degrees) { throw null; } [System.CLSCompliantAttribute(false)] public static uint EncodeBinary(System.Numerics.Decimal32 x) { throw null; } [System.CLSCompliantAttribute(false)] @@ -11877,6 +11880,7 @@ namespace System.Numerics public static System.Numerics.Decimal32 Parse(string s, System.IFormatProvider? provider) { throw null; } public static System.Numerics.Decimal32 Pow(System.Numerics.Decimal32 x, System.Numerics.Decimal32 y) { throw null; } public static System.Numerics.Decimal32 Quantize(System.Numerics.Decimal32 x, System.Numerics.Decimal32 y) { throw null; } + public static System.Numerics.Decimal32 RadiansToDegrees(System.Numerics.Decimal32 radians) { throw null; } public static System.Numerics.Decimal32 ReciprocalEstimate(System.Numerics.Decimal32 x) { throw null; } public static System.Numerics.Decimal32 ReciprocalSqrtEstimate(System.Numerics.Decimal32 x) { throw null; } public static System.Numerics.Decimal32 RootN(System.Numerics.Decimal32 x, int n) { throw null; } @@ -11985,6 +11989,7 @@ namespace System.Numerics public static System.Numerics.Decimal64 DecodeBinary(ulong x) { throw null; } [System.CLSCompliantAttribute(false)] public static System.Numerics.Decimal64 DecodeDecimal(ulong x) { throw null; } + public static System.Numerics.Decimal64 DegreesToRadians(System.Numerics.Decimal64 degrees) { throw null; } [System.CLSCompliantAttribute(false)] public static ulong EncodeBinary(System.Numerics.Decimal64 x) { throw null; } [System.CLSCompliantAttribute(false)] @@ -12129,6 +12134,7 @@ namespace System.Numerics public static System.Numerics.Decimal64 Parse(string s, System.IFormatProvider? provider) { throw null; } public static System.Numerics.Decimal64 Pow(System.Numerics.Decimal64 x, System.Numerics.Decimal64 y) { throw null; } public static System.Numerics.Decimal64 Quantize(System.Numerics.Decimal64 x, System.Numerics.Decimal64 y) { throw null; } + public static System.Numerics.Decimal64 RadiansToDegrees(System.Numerics.Decimal64 radians) { throw null; } public static System.Numerics.Decimal64 ReciprocalEstimate(System.Numerics.Decimal64 x) { throw null; } public static System.Numerics.Decimal64 ReciprocalSqrtEstimate(System.Numerics.Decimal64 x) { throw null; } public static System.Numerics.Decimal64 RootN(System.Numerics.Decimal64 x, int n) { throw null; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal128Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal128Tests.cs index 83441db6a4d1d1..a6fdb9b823733a 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal128Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal128Tests.cs @@ -2186,6 +2186,80 @@ public static void CbrtAccuracyTest(double input) Assert.True(double.Abs(actual - expected) <= 1e-13 * double.Abs(double.MaxMagnitude(expected, 1.0)), $"cbrt({input}): expected {expected}, got {actual}"); } + // The near-tie inputs are the significands whose exact product sits closest to a rounding + // boundary, which is where carrying the constant to fewer digits would decide the result + // differently; the expected values were computed independently at several hundred digits. + [Theory] + [InlineData(0x7C00000000000000UL, 0x0000000000000000UL, 0x7C00000000000000UL, 0x0000000000000000UL)] // NaN + [InlineData(0x7C00000000000000UL, 0x0000000000001234UL, 0x7C00000000000000UL, 0x0000000000001234UL)] // NaN payload + [InlineData(0x7800000000000000UL, 0x0000000000000000UL, 0x7800000000000000UL, 0x0000000000000000UL)] // +Infinity + [InlineData(0xF800000000000000UL, 0x0000000000000000UL, 0xF800000000000000UL, 0x0000000000000000UL)] // -Infinity + [InlineData(0x3040000000000000UL, 0x0000000000000000UL, 0x2FB2000000000000UL, 0x0000000000000000UL)] // +0 + [InlineData(0xB040000000000000UL, 0x0000000000000000UL, 0xAFB2000000000000UL, 0x0000000000000000UL)] // -0 + [InlineData(0x3040000000000000UL, 0x0000000000000001UL, 0x2FFA560D26F7C579UL, 0xA0A2A1380E8758A5UL)] // 1 + [InlineData(0xB040000000000000UL, 0x0000000000000001UL, 0xAFFA560D26F7C579UL, 0xA0A2A1380E8758A5UL)] // -1 + [InlineData(0x3040000000000000UL, 0x00000000000000B4UL, 0x2FFE9AE4795796A7UL, 0xBABE5564E6F39F8FUL)] // 180 + [InlineData(0x3040000000000000UL, 0x000000000000005AUL, 0x2FFE4D723CABCB53UL, 0xDD5F2AB27379CFC7UL)] // 90 + [InlineData(0x5FFFED09BEAD87C0UL, 0x378D8E63FFFFFFFFUL, 0x5FFC560D26F7C579UL, 0xA0A2A1380E8758A5UL)] // MaxValue + [InlineData(0xDFFFED09BEAD87C0UL, 0x378D8E63FFFFFFFFUL, 0xDFFC560D26F7C579UL, 0xA0A2A1380E8758A5UL)] // MinValue + [InlineData(0x0000000000000000UL, 0x0000000000000001UL, 0x0000000000000000UL, 0x0000000000000000UL)] // Epsilon + [InlineData(0x8000000000000000UL, 0x0000000000000001UL, 0x8000000000000000UL, 0x0000000000000000UL)] // -Epsilon + [InlineData(0x0000314DC6448D93UL, 0x38C15B0A00000000UL, 0x000000DC4AA1329DUL, 0x60538BDA291AF416UL)] // MinNormal + [InlineData(0x3041424057DBAFA1UL, 0x7A92CD84051F35CAUL, 0x303E383E53FE1D69UL, 0xC244F42B806A18A7UL)] // near tie 6536033685556752639387611326002634 + [InlineData(0x0003424057DBAFA1UL, 0x7A92CD84051F35CAUL, 0x0000383E53FE1D69UL, 0xC244F42B806A18A7UL)] // near tie 6536033685556752639387611326002634 subnormal + [InlineData(0x3040A1202BEDD7D0UL, 0xBD4966C2028F9AE5UL, 0x303D1937A3F69310UL, 0xCB58C4D982127B45UL)] // near tie 3268016842778376319693805663001317 + [InlineData(0x0002A1202BEDD7D0UL, 0xBD4966C2028F9AE5UL, 0x00001C1F29FF0EB4UL, 0xE1227A15C0350C54UL)] // near tie 3268016842778376319693805663001317 subnormal + [InlineData(0x3041AD735FA98270UL, 0x31CBA9395659E268UL, 0x303E4AF40A5D1831UL, 0x2955AEDE4CB68E84UL)] // near tie 8710294564656080819607388940788328 + [InlineData(0x0003AD735FA98270UL, 0x31CBA9395659E268UL, 0x00004AF40A5D1831UL, 0x2955AEDE4CB68E84UL)] // near tie 8710294564656080819607388940788328 subnormal + [InlineData(0x30409F6020CADAD9UL, 0x93467F291B180F08UL, 0x303D1629A8346BB7UL, 0x76E3D29AAB944773UL)] // near tie 3232519179338804268052849756737288 + [InlineData(0x00029F6020CADAD9UL, 0x93467F291B180F08UL, 0x00001BD0F738712BUL, 0xF249FB75DDF53A58UL)] // near tie 3232519179338804268052849756737288 subnormal + [InlineData(0x3041930F27224B48UL, 0x036B0A89E90CE783UL, 0x303E4658DB853295UL, 0xBC5F93615367893DUL)] // near tie 8175011604083703943360440171292547 + [InlineData(0x0003930F27224B48UL, 0x036B0A89E90CE783UL, 0x00004658DB853295UL, 0xBC5F93615367893DUL)] // near tie 8175011604083703943360440171292547 subnormal + [InlineData(0x3041DE206260908CUL, 0xB9D37D7B51482D18UL, 0x303E5372E5A95383UL, 0xD6DDF26199DFAF09UL)] // near tie 9697557538016412804158549270211864 + [InlineData(0x0003DE206260908CUL, 0xB9D37D7B51482D18UL, 0x00005372E5A95383UL, 0xD6DDF26199DFAF09UL)] // near tie 9697557538016412804158549270211864 subnormal + public static void DegreesToRadiansTest(ulong upper, ulong lower, ulong expectedUpper, ulong expectedLower) + { + Decimal128 result = Decimal128.DegreesToRadians(Unsafe.BitCast(new UInt128(upper, lower))); + Assert.Equal(new UInt128(expectedUpper, expectedLower), Unsafe.BitCast(result)); + } + + // The near-tie inputs are the significands whose exact product sits closest to a rounding + // boundary, which is where carrying the constant to fewer digits would decide the result + // differently; the expected values were computed independently at several hundred digits. + [Theory] + [InlineData(0x7C00000000000000UL, 0x0000000000000000UL, 0x7C00000000000000UL, 0x0000000000000000UL)] // NaN + [InlineData(0x7C00000000000000UL, 0x0000000000001234UL, 0x7C00000000000000UL, 0x0000000000001234UL)] // NaN payload + [InlineData(0x7800000000000000UL, 0x0000000000000000UL, 0x7800000000000000UL, 0x0000000000000000UL)] // +Infinity + [InlineData(0xF800000000000000UL, 0x0000000000000000UL, 0xF800000000000000UL, 0x0000000000000000UL)] // -Infinity + [InlineData(0x3040000000000000UL, 0x0000000000000000UL, 0x2FB8000000000000UL, 0x0000000000000000UL)] // +0 + [InlineData(0xB040000000000000UL, 0x0000000000000000UL, 0xAFB8000000000000UL, 0x0000000000000000UL)] // -0 + [InlineData(0x3040000000000000UL, 0x0000000000000001UL, 0x30011A7D70D76EF7UL, 0x8C6EBC7DA9DDC3D5UL)] // 1 + [InlineData(0xB040000000000000UL, 0x0000000000000001UL, 0xB0011A7D70D76EF7UL, 0x8C6EBC7DA9DDC3D5UL)] // -1 + [InlineData(0x3040000000000000UL, 0x00000000000000B4UL, 0x300632D923ABE5E4UL, 0xE0F536688A188EC5UL)] // 180 + [InlineData(0x3040000000000000UL, 0x000000000000005AUL, 0x3004FE3DB25B7D78UL, 0x64CA100AB27AC9D9UL)] // 90 + [InlineData(0x5FFFED09BEAD87C0UL, 0x378D8E63FFFFFFFFUL, 0x7800000000000000UL, 0x0000000000000000UL)] // MaxValue + [InlineData(0xDFFFED09BEAD87C0UL, 0x378D8E63FFFFFFFFUL, 0xF800000000000000UL, 0x0000000000000000UL)] // MinValue + [InlineData(0x0000000000000000UL, 0x0000000000000001UL, 0x0000000000000000UL, 0x0000000000000039UL)] // Epsilon + [InlineData(0x8000000000000000UL, 0x0000000000000001UL, 0x8000000000000000UL, 0x0000000000000039UL)] // -Epsilon + [InlineData(0x0000314DC6448D93UL, 0x38C15B0A00000000UL, 0x00031A7D70D76EF7UL, 0x8C6EBC7DA9DDC3D5UL)] // MinNormal + [InlineData(0x3041424057DBAFA1UL, 0x7A92CD84051F35CAUL, 0x3044B8A2EC436DA7UL, 0x0A75F543BBCE739DUL)] // near tie 6536033685556752639387611326002634 + [InlineData(0x0003424057DBAFA1UL, 0x7A92CD84051F35CAUL, 0x0006B8A2EC436DA7UL, 0x0A75F543BBCE739DUL)] // near tie 6536033685556752639387611326002634 subnormal + [InlineData(0x3040A1202BEDD7D0UL, 0xBD4966C2028F9AE5UL, 0x30445C517621B6D3UL, 0x853AFAA1DDE739CEUL)] // near tie 3268016842778376319693805663001317 + [InlineData(0x0002A1202BEDD7D0UL, 0xBD4966C2028F9AE5UL, 0x00065C517621B6D3UL, 0x853AFAA1DDE739CEUL)] // near tie 3268016842778376319693805663001317 subnormal + [InlineData(0x3041AD735FA98270UL, 0x31CBA9395659E268UL, 0x3044F60E9F0327EAUL, 0x2C110DC3E910F05BUL)] // near tie 8710294564656080819607388940788328 + [InlineData(0x0003AD735FA98270UL, 0x31CBA9395659E268UL, 0x0006F60E9F0327EAUL, 0x2C110DC3E910F05BUL)] // near tie 8710294564656080819607388940788328 subnormal + [InlineData(0x30409F6020CADAD9UL, 0x93467F291B180F08UL, 0x30445B50C05E03BFUL, 0xB535FF7823E00781UL)] // near tie 3232519179338804268052849756737288 + [InlineData(0x00029F6020CADAD9UL, 0x93467F291B180F08UL, 0x00065B50C05E03BFUL, 0xB535FF7823E00781UL)] // near tie 3232519179338804268052849756737288 subnormal + [InlineData(0x3041930F27224B48UL, 0x036B0A89E90CE783UL, 0x3044E6EF977DABE1UL, 0xC55CCE0BE13F2E49UL)] // near tie 8175011604083703943360440171292547 + [InlineData(0x0003930F27224B48UL, 0x036B0A89E90CE783UL, 0x0006E6EF977DABE1UL, 0xC55CCE0BE13F2E49UL)] // near tie 8175011604083703943360440171292547 subnormal + [InlineData(0x3041DE206260908CUL, 0xB9D37D7B51482D18UL, 0x304511F2411A0B3FUL, 0x1FA1FE686BA01684UL)] // near tie 9697557538016412804158549270211864 + [InlineData(0x0003DE206260908CUL, 0xB9D37D7B51482D18UL, 0x000711F2411A0B3FUL, 0x1FA1FE686BA01684UL)] // near tie 9697557538016412804158549270211864 subnormal + public static void RadiansToDegreesTest(ulong upper, ulong lower, ulong expectedUpper, ulong expectedLower) + { + Decimal128 result = Decimal128.RadiansToDegrees(Unsafe.BitCast(new UInt128(upper, lower))); + Assert.Equal(new UInt128(expectedUpper, expectedLower), Unsafe.BitCast(result)); + } + [Theory] [InlineData(0x7C00000000000000UL, 0x0000000000000000UL, 0x7800000000000000UL, 0x0000000000000000UL, 0x7800000000000000UL, 0x0000000000000000UL)] // hypot(NaN, +Infinity) = +Infinity [InlineData(0x7800000000000000UL, 0x0000000000000000UL, 0x7C00000000000000UL, 0x0000000000000000UL, 0x7800000000000000UL, 0x0000000000000000UL)] // hypot(+Infinity, NaN) = +Infinity diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal32Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal32Tests.cs index 595ff39be865c1..64dd72c5cbd4b3 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal32Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal32Tests.cs @@ -2173,6 +2173,78 @@ public static void CbrtAccuracyTest(double input) Assert.True(double.Abs(actual - expected) <= 5e-7 * double.Abs(double.MaxMagnitude(expected, 1.0)), $"cbrt({input}): expected {expected}, got {actual}"); } + // The near-tie inputs are the significands whose exact product sits closest to a rounding + // boundary, which is where carrying the constant to fewer digits would decide the result + // differently; the expected values were computed independently at several hundred digits. + [Theory] + [InlineData(0x7C000000U, 0x7C000000U)] // NaN + [InlineData(0x7C001234U, 0x7C001234U)] // NaN payload + [InlineData(0x78000000U, 0x78000000U)] // +Infinity + [InlineData(0xF8000000U, 0xF8000000U)] // -Infinity + [InlineData(0x32800000U, 0x2A000000U)] // +0 + [InlineData(0xB2800000U, 0xAA000000U)] // -0 + [InlineData(0x32800001U, 0x2E9AA1B1U)] // 1 + [InlineData(0xB2800001U, 0xAE9AA1B1U)] // -1 + [InlineData(0x328000B4U, 0x2FAFEFD9U)] // 180 + [InlineData(0x3280005AU, 0x2F97F7ECU)] // 90 + [InlineData(0x77F8967FU, 0x5F1AA1B1U)] // MaxValue + [InlineData(0xF7F8967FU, 0xDF1AA1B1U)] // MinValue + [InlineData(0x00000001U, 0x00000000U)] // Epsilon + [InlineData(0x80000001U, 0x80000000U)] // -Epsilon + [InlineData(0x000F4240U, 0x0000442DU)] // MinNormal + [InlineData(0x32E53CFFU, 0x3211AB5EU)] // near tie 6634751 + [InlineData(0x00E53CFFU, 0x0011AB5EU)] // near tie 6634751 subnormal + [InlineData(0x32C8BD57U, 0x31FEF45EU)] // near tie 4767063 + [InlineData(0x00C8BD57U, 0x000CB209U)] // near tie 4767063 subnormal + [InlineData(0x32AA4456U, 0x31C9C50CU)] // near tie 2770006 + [InlineData(0x00AA4456U, 0x00076081U)] // near tie 2770006 subnormal + [InlineData(0x32CFC6FBU, 0x6C6B3CD3U)] // near tie 5228283 + [InlineData(0x00CFC6FBU, 0x000DEC7CU)] // near tie 5228283 subnormal + [InlineData(0x6CA4F64DU, 0x321734CEU)] // near tie 8713805 + [InlineData(0x6024F64DU, 0x001734CEU)] // near tie 8713805 subnormal + [InlineData(0x32F8F270U, 0x32151BF7U)] // near tie 7926384 + [InlineData(0x00F8F270U, 0x00151BF7U)] // near tie 7926384 subnormal + public static void DegreesToRadiansTest(uint value, uint expected) + { + Assert.Equal(expected, Unsafe.BitCast(Decimal32.DegreesToRadians(Unsafe.BitCast(value)))); + } + + // The near-tie inputs are the significands whose exact product sits closest to a rounding + // boundary, which is where carrying the constant to fewer digits would decide the result + // differently; the expected values were computed independently at several hundred digits. + [Theory] + [InlineData(0x7C000000U, 0x7C000000U)] // NaN + [InlineData(0x7C001234U, 0x7C001234U)] // NaN payload + [InlineData(0x78000000U, 0x78000000U)] // +Infinity + [InlineData(0xF8000000U, 0xF8000000U)] // -Infinity + [InlineData(0x32800000U, 0x2B800000U)] // +0 + [InlineData(0xB2800000U, 0xAB800000U)] // -0 + [InlineData(0x32800001U, 0x30576D2AU)] // 1 + [InlineData(0xB2800001U, 0xB0576D2AU)] // -1 + [InlineData(0x328000B4U, 0x318FBC9CU)] // 180 + [InlineData(0x3280005AU, 0x314EAF0CU)] // 90 + [InlineData(0x77F8967FU, 0x78000000U)] // MaxValue + [InlineData(0xF7F8967FU, 0xF8000000U)] // MinValue + [InlineData(0x00000001U, 0x00000039U)] // Epsilon + [InlineData(0x80000001U, 0x80000039U)] // -Epsilon + [InlineData(0x000F4240U, 0x00D76D2AU)] // MinNormal + [InlineData(0x32E53CFFU, 0x33BA0158U)] // near tie 6634751 + [InlineData(0x00E53CFFU, 0x01BA0158U)] // near tie 6634751 subnormal + [InlineData(0x32C8BD57U, 0x33A9AD3EU)] // near tie 4767063 + [InlineData(0x00C8BD57U, 0x01A9AD3EU)] // near tie 4767063 subnormal + [InlineData(0x32AA4456U, 0x33983799U)] // near tie 2770006 + [InlineData(0x00AA4456U, 0x01983799U)] // near tie 2770006 subnormal + [InlineData(0x32CFC6FBU, 0x33ADB581U)] // near tie 5228283 + [InlineData(0x00CFC6FBU, 0x01ADB581U)] // near tie 5228283 subnormal + [InlineData(0x6CA4F64DU, 0x33CC2E82U)] // near tie 8713805 + [InlineData(0x6024F64DU, 0x01CC2E82U)] // near tie 8713805 subnormal + [InlineData(0x32F8F270U, 0x33C54C2CU)] // near tie 7926384 + [InlineData(0x00F8F270U, 0x01C54C2CU)] // near tie 7926384 subnormal + public static void RadiansToDegreesTest(uint value, uint expected) + { + Assert.Equal(expected, Unsafe.BitCast(Decimal32.RadiansToDegrees(Unsafe.BitCast(value)))); + } + [Theory] [InlineData(0x7C000000U, 0x78000000U, 0x78000000U)] // hypot(NaN, +Infinity) = +Infinity [InlineData(0x78000000U, 0x7C000000U, 0x78000000U)] // hypot(+Infinity, NaN) = +Infinity diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs index be4774fd00dff4..8bd8bb011da8e8 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs @@ -2176,6 +2176,78 @@ public static void CbrtAccuracyTest(double input) Assert.True(double.Abs(actual - expected) <= 1e-13 * double.Abs(double.MaxMagnitude(expected, 1.0)), $"cbrt({input}): expected {expected}, got {actual}"); } + // The near-tie inputs are the significands whose exact product sits closest to a rounding + // boundary, which is where carrying the constant to fewer digits would decide the result + // differently; the expected values were computed independently at several hundred digits. + [Theory] + [InlineData(0x7C00000000000000UL, 0x7C00000000000000UL)] // NaN + [InlineData(0x7C00000000001234UL, 0x7C00000000001234UL)] // NaN payload + [InlineData(0x7800000000000000UL, 0x7800000000000000UL)] // +Infinity + [InlineData(0xF800000000000000UL, 0xF800000000000000UL)] // -Infinity + [InlineData(0x31C0000000000000UL, 0x2D60000000000000UL)] // +0 + [InlineData(0xB1C0000000000000UL, 0xAD60000000000000UL)] // -0 + [InlineData(0x31C0000000000001UL, 0x2FA6335E2214CADAUL)] // 1 + [InlineData(0xB1C0000000000001UL, 0xAFA6335E2214CADAUL)] // -1 + [InlineData(0x31C00000000000B4UL, 0x2FEB29430A256D21UL)] // 180 + [InlineData(0x31C000000000005AUL, 0x2FE594A18512B691UL)] // 90 + [InlineData(0x77FB86F26FC0FFFFUL, 0x5FC6335E2214CAD9UL)] // MaxValue + [InlineData(0xF7FB86F26FC0FFFFUL, 0xDFC6335E2214CAD9UL)] // MinValue + [InlineData(0x0000000000000001UL, 0x0000000000000000UL)] // Epsilon + [InlineData(0x8000000000000001UL, 0x8000000000000000UL)] // -Epsilon + [InlineData(0x00038D7EA4C68000UL, 0x00000FDFA94D0207UL)] // MinNormal + [InlineData(0x31D3C7CB9E6193D6UL, 0x6C6285FD7E8E7308UL)] // near tie 5567701906985942 + [InlineData(0x0033C7CB9E6193D6UL, 0x000373CC8CA7D84EUL)] // near tie 5567701906985942 subnormal + [InlineData(0x31CBBEE7DBB055BFUL, 0x3194800A5C2EE663UL)] // near tie 3306127776306623 + [InlineData(0x002BBEE7DBB055BFUL, 0x00020CCDD604B0A3UL)] // near tie 3306127776306623 subnormal + [InlineData(0x6C70C06A9B780723UL, 0x31A5B75B526CB460UL)] // near tie 9218763362141987 + [InlineData(0x6008C06A9B780723UL, 0x0005B75B526CB460UL)] // near tie 9218763362141987 subnormal + [InlineData(0x31C702F9912FC653UL, 0x318C3CD2786CBFD9UL)] // near tie 1973595742914131 + [InlineData(0x002702F9912FC653UL, 0x000139483F3E132FUL)] // near tie 1973595742914131 subnormal + [InlineData(0x31C4CD1B075AF51CUL, 0x31886131A50584C7UL)] // near tie 1351415878055196 + [InlineData(0x0024CD1B075AF51CUL, 0x0000D684F6E6F3AEUL)] // near tie 1351415878055196 subnormal + [InlineData(0x31D508ECB38F52F9UL, 0x31A3ABD8BDBA398EUL)] // near tie 5920787228742393 + [InlineData(0x003508ECB38F52F9UL, 0x0003ABD8BDBA398EUL)] // near tie 5920787228742393 subnormal + public static void DegreesToRadiansTest(ulong value, ulong expected) + { + Assert.Equal(expected, Unsafe.BitCast(Decimal64.DegreesToRadians(Unsafe.BitCast(value)))); + } + + // The near-tie inputs are the significands whose exact product sits closest to a rounding + // boundary, which is where carrying the constant to fewer digits would decide the result + // differently; the expected values were computed independently at several hundred digits. + [Theory] + [InlineData(0x7C00000000000000UL, 0x7C00000000000000UL)] // NaN + [InlineData(0x7C00000000001234UL, 0x7C00000000001234UL)] // NaN payload + [InlineData(0x7800000000000000UL, 0x7800000000000000UL)] // +Infinity + [InlineData(0xF800000000000000UL, 0xF800000000000000UL)] // -Infinity + [InlineData(0x31C0000000000000UL, 0x2DC0000000000000UL)] // +0 + [InlineData(0xB1C0000000000000UL, 0xADC0000000000000UL)] // -0 + [InlineData(0x31C0000000000001UL, 0x30145B05528029C8UL)] // 1 + [InlineData(0xB1C0000000000001UL, 0xB0145B05528029C8UL)] // -1 + [InlineData(0x31C00000000000B4UL, 0x3063A9FBD687B59AUL)] // 180 + [InlineData(0x31C000000000005AUL, 0x305251EB30A68C01UL)] // 90 + [InlineData(0x77FB86F26FC0FFFFUL, 0x7800000000000000UL)] // MaxValue + [InlineData(0xF7FB86F26FC0FFFFUL, 0xF800000000000000UL)] // MinValue + [InlineData(0x0000000000000001UL, 0x0000000000000039UL)] // Epsilon + [InlineData(0x8000000000000001UL, 0x8000000000000039UL)] // -Epsilon + [InlineData(0x00038D7EA4C68000UL, 0x00345B05528029C8UL)] // MinNormal + [InlineData(0x31D3C7CB9E6193D6UL, 0x320B55574E55B3B9UL)] // near tie 5567701906985942 + [InlineData(0x0033C7CB9E6193D6UL, 0x006B55574E55B3B9UL)] // near tie 5567701906985942 subnormal + [InlineData(0x31CBBEE7DBB055BFUL, 0x3206BAD47EA97B66UL)] // near tie 3306127776306623 + [InlineData(0x002BBEE7DBB055BFUL, 0x0066BAD47EA97B66UL)] // near tie 3306127776306623 subnormal + [InlineData(0x6C70C06A9B780723UL, 0x3212C3EAB0629378UL)] // near tie 9218763362141987 + [InlineData(0x6008C06A9B780723UL, 0x0072C3EAB0629378UL)] // near tie 9218763362141987 subnormal + [InlineData(0x31C702F9912FC653UL, 0x32040471E1589F0CUL)] // near tie 1973595742914131 + [InlineData(0x002702F9912FC653UL, 0x00640471E1589F0CUL)] // near tie 1973595742914131 subnormal + [InlineData(0x31C4CD1B075AF51CUL, 0x31FB824198B94A89UL)] // near tie 1351415878055196 + [InlineData(0x0024CD1B075AF51CUL, 0x005B824198B94A89UL)] // near tie 1351415878055196 subnormal + [InlineData(0x31D508ECB38F52F9UL, 0x320C0D55A409DD23UL)] // near tie 5920787228742393 + [InlineData(0x003508ECB38F52F9UL, 0x006C0D55A409DD23UL)] // near tie 5920787228742393 subnormal + public static void RadiansToDegreesTest(ulong value, ulong expected) + { + Assert.Equal(expected, Unsafe.BitCast(Decimal64.RadiansToDegrees(Unsafe.BitCast(value)))); + } + [Theory] [InlineData(0x7C00000000000000UL, 0x7800000000000000UL, 0x7800000000000000UL)] // hypot(NaN, +Infinity) = +Infinity [InlineData(0x7800000000000000UL, 0x7C00000000000000UL, 0x7800000000000000UL)] // hypot(+Infinity, NaN) = +Infinity diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs index f0238fe488fb85..ed93be387ab5fb 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs @@ -1906,6 +1906,46 @@ public static void RadiansToDegreesTest(double value, double expectedResult, dou AssertExtensions.Equal(+expectedResult, double.RadiansToDegrees(+value), allowedVariance); } + // Both conversions are correctly rounded, so these compare bits rather than allowing a + // variance. The inputs are the ones that pick a path or a sign the bulk data cannot reach: + // zero, the subnormal range where the middle term underflows, the cutoffs at either end, + // and an overflow. The last two are the inputs whose exact product lands closest to a + // rounding midpoint anywhere in the domain, at 2^-55.58 and 2^-55.99 ulp, so they are what + // a form carrying too few bits gets wrong first. + [Theory] + [InlineData(0x0000_0000_0000_0000, 0x0000_0000_0000_0000, 0x0000_0000_0000_0000)] // 0 + [InlineData(0x0000_0000_0000_0001, 0x0000_0000_0000_0000, 0x0000_0000_0000_0039)] // Epsilon + [InlineData(0x0010_0000_0000_0000, 0x0000_477D_1A89_4A75, 0x006C_A5DC_1A63_C1F8)] // MinNormal + [InlineData(0x031F_FFFF_FFFF_FFFF, 0x02C1_DF46_A252_9D39, 0x037C_A5DC_1A63_C1F7)] // RadiansToDegreesMin - 1 ulp + [InlineData(0x0320_0000_0000_0000, 0x02C1_DF46_A252_9D39, 0x037C_A5DC_1A63_C1F8)] // RadiansToDegreesMin + [InlineData(0x03EF_FFFF_FFFF_FFFF, 0x0391_DF46_A252_9D39, 0x044C_A5DC_1A63_C1F7)] // DegreesToRadiansMin - 1 ulp + [InlineData(0x03F0_0000_0000_0000, 0x0391_DF46_A252_9D39, 0x044C_A5DC_1A63_C1F8)] // DegreesToRadiansMin + [InlineData(0x7F91_DF46_A252_9D38, 0x7F33_F6A1_DB14_1FB8, 0x7FEF_FFFF_FFFF_FFFE)] // RadiansToDegreesMax - 1 ulp + [InlineData(0x7F91_DF46_A252_9D39, 0x7F33_F6A1_DB14_1FB9, 0x7FF0_0000_0000_0000)] // RadiansToDegreesMax + [InlineData(0x7FEF_FFFF_FFFF_FFFF, 0x7F91_DF46_A252_9D39, 0x7FF0_0000_0000_0000)] // MaxValue + [InlineData(0x7FF0_0000_0000_0000, 0x7FF0_0000_0000_0000, 0x7FF0_0000_0000_0000)] // PositiveInfinity + [InlineData(0x3FF9_6BDF_4AA9_CD3B, 0x3F9C_655C_F14D_66CB, 0x4056_C226_7343_95AC)] // closest to a midpoint anywhere for DegreesToRadians + [InlineData(0x3FFD_B0FB_3010_78BE, 0x3FA0_9530_51C1_CB1B, 0x405A_94C0_9279_849F)] // closest to a midpoint anywhere for RadiansToDegrees + public static void DegreesToRadiansRadiansToDegreesEdgeTest(ulong valueBits, ulong degreesToRadiansBits, ulong radiansToDegreesBits) + { + const ulong SignMask = 0x8000_0000_0000_0000; + + double value = BitConverter.UInt64BitsToDouble(valueBits); + double degreesToRadians = BitConverter.UInt64BitsToDouble(degreesToRadiansBits); + double radiansToDegrees = BitConverter.UInt64BitsToDouble(radiansToDegreesBits); + + AssertExtensions.Equal(degreesToRadians, double.DegreesToRadians(value)); + AssertExtensions.Equal(radiansToDegrees, double.RadiansToDegrees(value)); + + // Negating flips only the sign bit, which pins the sign of a zero result + double negativeValue = BitConverter.UInt64BitsToDouble(valueBits ^ SignMask); + double negativeDegreesToRadians = BitConverter.UInt64BitsToDouble(degreesToRadiansBits ^ SignMask); + double negativeRadiansToDegrees = BitConverter.UInt64BitsToDouble(radiansToDegreesBits ^ SignMask); + + AssertExtensions.Equal(negativeDegreesToRadians, double.DegreesToRadians(negativeValue)); + AssertExtensions.Equal(negativeRadiansToDegrees, double.RadiansToDegrees(negativeValue)); + } + public static IEnumerable TryParsePartial_TestData() { // Basic floating point parsing with trailing invalid characters diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs index 927bea6948cbe5..3724ae20b8e7de 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs @@ -2598,6 +2598,33 @@ public static void RadiansToDegreesTest(Half value, Half expectedResult, Half al AssertExtensions.Equal(+expectedResult, Half.RadiansToDegrees(+value), allowedVariance); } + // Both conversions are correctly rounded, so these compare bits rather than allowing a + // variance. The inputs are the ones the bulk data cannot reach: zero, the subnormal range + // on either side of the conversion, and an overflow. + [Theory] + [InlineData(0x0000, 0x0000, 0x0000)] // 0 + [InlineData(0x0001, 0x0000, 0x0039)] // Epsilon + [InlineData(0x0200, 0x0009, 0x1729)] // 0x1p-15 + [InlineData(0x0400, 0x0012, 0x1B29)] // MinNormal + [InlineData(0x3C00, 0x2478, 0x5329)] // One + [InlineData(0x7BFF, 0x6477, 0x7C00)] // MaxValue, overflows for RadiansToDegrees + [InlineData(0x7C00, 0x7C00, 0x7C00)] // PositiveInfinity + public static void DegreesToRadiansRadiansToDegreesEdgeTest(ushort valueBits, ushort degreesToRadiansBits, ushort radiansToDegreesBits) + { + const ushort SignMask = 0x8000; + + Half value = BitConverter.UInt16BitsToHalf(valueBits); + + AssertExtensions.Equal(BitConverter.UInt16BitsToHalf(degreesToRadiansBits), Half.DegreesToRadians(value)); + AssertExtensions.Equal(BitConverter.UInt16BitsToHalf(radiansToDegreesBits), Half.RadiansToDegrees(value)); + + // Negating flips only the sign bit, which pins the sign of a zero result + Half negativeValue = BitConverter.UInt16BitsToHalf((ushort)(valueBits ^ SignMask)); + + AssertExtensions.Equal(BitConverter.UInt16BitsToHalf((ushort)(degreesToRadiansBits ^ SignMask)), Half.DegreesToRadians(negativeValue)); + AssertExtensions.Equal(BitConverter.UInt16BitsToHalf((ushort)(radiansToDegreesBits ^ SignMask)), Half.RadiansToDegrees(negativeValue)); + } + [Theory] [InlineData(float.PositiveInfinity, int.MaxValue)] [InlineData(float.NaN, int.MaxValue)] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs index a5ff7414edba4f..ddd402a632ba89 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs @@ -2594,6 +2594,33 @@ public static void RadiansToDegreesTest(BFloat16 value, BFloat16 expectedResult, AssertEqual(+expectedResult, BFloat16.RadiansToDegrees(+value), allowedVariance); } + // Both conversions are correctly rounded, so these compare bits rather than allowing a + // variance. The inputs are the ones the bulk data cannot reach: zero, the subnormal range + // on either side of the conversion, and an overflow. + [Theory] + [InlineData(0x0000, 0x0000, 0x0000)] // 0 + [InlineData(0x0001, 0x0000, 0x0039)] // Epsilon + [InlineData(0x0040, 0x0001, 0x02E5)] // 0x1p-133 + [InlineData(0x0080, 0x0002, 0x0365)] // MinNormal + [InlineData(0x3F80, 0x3C8F, 0x4265)] // One + [InlineData(0x7F7F, 0x7C8E, 0x7F80)] // MaxValue, overflows for RadiansToDegrees + [InlineData(0x7F80, 0x7F80, 0x7F80)] // PositiveInfinity + public static void DegreesToRadiansRadiansToDegreesEdgeTest(ushort valueBits, ushort degreesToRadiansBits, ushort radiansToDegreesBits) + { + const ushort SignMask = 0x8000; + + BFloat16 value = BitConverter.UInt16BitsToBFloat16(valueBits); + + AssertEqual(BitConverter.UInt16BitsToBFloat16(degreesToRadiansBits), BFloat16.DegreesToRadians(value)); + AssertEqual(BitConverter.UInt16BitsToBFloat16(radiansToDegreesBits), BFloat16.RadiansToDegrees(value)); + + // Negating flips only the sign bit, which pins the sign of a zero result + BFloat16 negativeValue = BitConverter.UInt16BitsToBFloat16((ushort)(valueBits ^ SignMask)); + + AssertEqual(BitConverter.UInt16BitsToBFloat16((ushort)(degreesToRadiansBits ^ SignMask)), BFloat16.DegreesToRadians(negativeValue)); + AssertEqual(BitConverter.UInt16BitsToBFloat16((ushort)(radiansToDegreesBits ^ SignMask)), BFloat16.RadiansToDegrees(negativeValue)); + } + [Theory] [InlineData(float.PositiveInfinity, int.MaxValue)] [InlineData(float.NaN, int.MaxValue)] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs index 0d83d4d1708956..bdaf7325031183 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs @@ -1656,6 +1656,32 @@ public static void RadiansToDegreesTest(float value, float expectedResult, float AssertExtensions.Equal(+expectedResult, float.RadiansToDegrees(+value), allowedVariance); } + // Both conversions are correctly rounded, so these compare bits rather than allowing a + // variance. The inputs are the ones the bulk data cannot reach: zero, the subnormal range + // on either side of the conversion, and an overflow. + [Theory] + [InlineData(0x0000_0000, 0x0000_0000, 0x0000_0000)] // 0 + [InlineData(0x0000_0001, 0x0000_0000, 0x0000_0039)] // Epsilon + [InlineData(0x0040_0000, 0x0001_1DF4, 0x02E5_2EE1)] // 0x1p-127 + [InlineData(0x0080_0000, 0x0002_3BE9, 0x0365_2EE1)] // MinNormal + [InlineData(0x7F7F_FFFF, 0x7C8E_FA35, 0x7F80_0000)] // MaxValue, overflows for RadiansToDegrees + [InlineData(0x7F80_0000, 0x7F80_0000, 0x7F80_0000)] // PositiveInfinity + public static void DegreesToRadiansRadiansToDegreesEdgeTest(uint valueBits, uint degreesToRadiansBits, uint radiansToDegreesBits) + { + const uint SignMask = 0x8000_0000; + + float value = BitConverter.UInt32BitsToSingle(valueBits); + + AssertExtensions.Equal(BitConverter.UInt32BitsToSingle(degreesToRadiansBits), float.DegreesToRadians(value)); + AssertExtensions.Equal(BitConverter.UInt32BitsToSingle(radiansToDegreesBits), float.RadiansToDegrees(value)); + + // Negating flips only the sign bit, which pins the sign of a zero result + float negativeValue = BitConverter.UInt32BitsToSingle(valueBits ^ SignMask); + + AssertExtensions.Equal(BitConverter.UInt32BitsToSingle(degreesToRadiansBits ^ SignMask), float.DegreesToRadians(negativeValue)); + AssertExtensions.Equal(BitConverter.UInt32BitsToSingle(radiansToDegreesBits ^ SignMask), float.RadiansToDegrees(negativeValue)); + } + public static IEnumerable TryParsePartial_TestData() { // Basic floating point parsing with trailing invalid characters