Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,60 +113,21 @@ public static void Round<T>(ReadOnlySpan<T> x, int digits, MidpointRounding mode
return;
}

ReadOnlySpan<T> roundPower10;
if (typeof(T) == typeof(float))
{
ReadOnlySpan<float> roundPower10Single = [1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f];
roundPower10 = Rename<float, T>(roundPower10Single);
}
else if (typeof(T) == typeof(double))
{
Debug.Assert(typeof(T) == typeof(double));
ReadOnlySpan<double> roundPower10Double = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15];
roundPower10 = Rename<double, T>(roundPower10Double);
}
else
{
if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity)
{
throw new ArgumentException(SR.Format(SR.Argument_InvalidEnumValue, mode, typeof(MidpointRounding)), nameof(mode));
}

InvokeSpanIntoSpan(x, new RoundFallbackOperator<T>(digits, mode), destination);
return;
}

if ((uint)digits >= (uint)roundPower10.Length)
if (digits < 0)
{
throw new ArgumentOutOfRangeException(nameof(digits));
}

T power10 = roundPower10[digits];
switch (mode)
if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity)
{
case MidpointRounding.ToEven:
InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator<T, RoundToEvenOperator<T>>(power10), destination);
return;

case MidpointRounding.AwayFromZero:
InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator<T, RoundAwayFromZeroOperator<T>>(power10), destination);
return;

case MidpointRounding.ToZero:
InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator<T, TruncateOperator<T>>(power10), destination);
return;

case MidpointRounding.ToNegativeInfinity:
InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator<T, FloorOperator<T>>(power10), destination);
return;

case MidpointRounding.ToPositiveInfinity:
InvokeSpanIntoSpan(x, new MultiplyRoundDivideOperator<T, CeilingOperator<T>>(power10), destination);
return;

default:
throw new ArgumentException(SR.Format(SR.Argument_InvalidEnumValue, mode, typeof(MidpointRounding)), nameof(mode));
throw new ArgumentException(SR.Format(SR.Argument_InvalidEnumValue, mode, typeof(MidpointRounding)), nameof(mode));
}

// The digit-based rounding defers to the scalar `T.Round` for every element, which accepts any
// non-negative `digits` (matching the scalar API). A correctly-rounded vectorized implementation
// needs the exact (e.g. double-double or arbitrary precision) scaled value to match the scalar
// result at the midpoints, so that acceleration is left as a future improvement.
InvokeSpanIntoSpan(x, new RoundFallbackOperator<T>(digits, mode), destination);
}

/// <summary>T.Round(x)</summary>
Expand Down Expand Up @@ -279,57 +240,6 @@ public static Vector512<T> Invoke(Vector512<T> x)
}
}

/// <summary>(T.Round(x * power10, digits, mode)) / power10</summary>
private readonly struct MultiplyRoundDivideOperator<T, TDelegatedRound> : IStatefulUnaryOperator<T>
where T : IFloatingPoint<T>
where TDelegatedRound : IUnaryOperator<T, T>
{
private readonly T _factor;

public MultiplyRoundDivideOperator(T factor)
{
Debug.Assert(typeof(T) == typeof(float) || typeof(T) == typeof(double));
_factor = factor;
}

public static bool Vectorizable => true;

private const float Single_RoundLimit = 1e8f;
private const double Double_RoundLimit = 1e16d;

public T Invoke(T x)
{
T limit = typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit);
return T.Abs(x) < limit ?
TDelegatedRound.Invoke(x * _factor) / _factor :
x;
}

public Vector128<T> Invoke(Vector128<T> x)
{
Vector128<T> limit = Vector128.Create(typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit));
return Vector128.ConditionalSelect(Vector128.LessThan(Vector128.Abs(x), limit),
TDelegatedRound.Invoke(x * _factor) / _factor,
x);
}

public Vector256<T> Invoke(Vector256<T> x)
{
Vector256<T> limit = Vector256.Create(typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit));
return Vector256.ConditionalSelect(Vector256.LessThan(Vector256.Abs(x), limit),
TDelegatedRound.Invoke(x * _factor) / _factor,
x);
}

public Vector512<T> Invoke(Vector512<T> x)
{
Vector512<T> limit = Vector512.Create(typeof(T) == typeof(float) ? T.CreateTruncating(Single_RoundLimit) : T.CreateTruncating(Double_RoundLimit));
return Vector512.ConditionalSelect(Vector512.LessThan(Vector512.Abs(x), limit),
TDelegatedRound.Invoke(x * _factor) / _factor,
x);
}
}

/// <summary>T.Round(x, digits, mode)</summary>
private readonly struct RoundFallbackOperator<T>(int digits, MidpointRounding mode) : IStatefulUnaryOperator<T>
where T : IFloatingPoint<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1666,7 +1666,7 @@ public static IEnumerable<object[]> RoundData()
{
foreach (MidpointRounding mode in Enum.GetValues(typeof(MidpointRounding)))
{
foreach (int digits in new[] { 0, 1, 4 })
foreach (int digits in new[] { 0, 1, 4, 20 })
{
yield return new object[] { mode, digits };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1935,10 +1935,7 @@
<value>Valid values are between {0} and {1}, inclusive.</value>
</data>
<data name="ArgumentOutOfRange_RoundingDigits" xml:space="preserve">
<value>Rounding digits must be between 0 and 15, inclusive.</value>
</data>
<data name="ArgumentOutOfRange_RoundingDigits_MathF" xml:space="preserve">
<value>Rounding digits must be between 0 and 6, inclusive.</value>
<value>Rounding digits must be greater than or equal to 0.</value>
</data>
<data name="ArgumentOutOfRange_SmallCapacity" xml:space="preserve">
<value>capacity was less than the current size.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Number.Grisu3.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Number.NumberToFloatingPointBits.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Number.Parsing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Number.Rounding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\BFloat16.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\BitOperations.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\Colors\Argb.cs" />
Expand Down
43 changes: 30 additions & 13 deletions src/libraries/System.Private.CoreLib/src/System/Math.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ public static partial class Math

public const double Tau = 6.283185307179586476925;

private const int maxRoundingDigits = 15;
// The largest digit count the fast rounding path handles: `10^digits` must fit a `ulong` for the
// integer fallback (10^19 is the last that does); larger counts use the exact routine.
private const int maxFastRoundingDigits = 19;

private const double doubleRoundLimit = 1e16d;

// This table is required for the Round function which can specify the number of digits to round to
private static ReadOnlySpan<double> RoundPower10Double =>
[
1E0, 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8,
1E9, 1E10, 1E11, 1E12, 1E13, 1E14, 1E15
];
// Below this boundary a double may have a fractional portion; at or above it every
// representable value is already an integer (2^52).
private const double doubleIntegerBoundary = 4503599627370496.0;

private const double SCALEB_C1 = 8.98846567431158E+307; // 0x1p1023

Expand Down Expand Up @@ -1406,15 +1403,35 @@ public static double Round(double value, MidpointRounding mode)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Round(double value, int digits, MidpointRounding mode)
{
if ((uint)digits > maxRoundingDigits)
if (digits < 0)
{
ThrowHelper.ThrowArgumentOutOfRange_RoundingDigits(nameof(digits));
}

if (Abs(value) < doubleRoundLimit)
if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity)
{
double power10 = RoundPower10Double[digits];
value = Round(value * power10, mode) / power10;
ThrowHelper.ThrowArgumentException_InvalidEnumValue(mode);
}

// Rounding to zero fractional digits is just rounding to an integer, which the dedicated
// overload does with a single hardware instruction on most platforms.
if (digits == 0)
{
return Round(value, mode);
}

// Only finite values with a magnitude below the integer boundary can have a fractional
// portion to round. All other values (including NaN and Infinity) are returned unchanged;
// this comparison is naturally false for those cases.
if (Abs(value) < doubleIntegerBoundary)
{
// The fast path only handles the small-digit range where `10^digits` is exactly
// representable; larger counts fall back to the exact arbitrary-precision routine.
if ((digits > maxFastRoundingDigits) || !Number.TryRoundToDecimalDigitsFast(value, digits, mode, out double rounded))
{
rounded = Number.RoundToDecimalDigits<double>(value, digits, mode);
}
value = rounded;
}

return value;
Expand Down
44 changes: 31 additions & 13 deletions src/libraries/System.Private.CoreLib/src/System/MathF.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ public static partial class MathF

public const float Tau = 6.283185307f;

private const int maxRoundingDigits = 6;
// The largest digit count the fast rounding path handles: `10^digits` must be exactly representable
// as a `float` (10^10 is the last that is); larger counts use the exact routine.
private const int maxFastRoundingDigits = 10;

// This table is required for the Round function which can specify the number of digits to round to
private static ReadOnlySpan<float> RoundPower10Single =>
[
1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f
];

private const float singleRoundLimit = 1e8f;
// Below this boundary a float may have a fractional portion; at or above it every
// representable value is already an integer (2^23).
private const float singleIntegerBoundary = 8388608.0f;

private const float SCALEB_C1 = 1.7014118E+38f; // 0x1p127f

Expand Down Expand Up @@ -429,15 +427,35 @@ public static float Round(float x, MidpointRounding mode)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Round(float x, int digits, MidpointRounding mode)
{
if ((uint)digits > maxRoundingDigits)
if (digits < 0)
{
ThrowHelper.ThrowArgumentOutOfRange_RoundingDigits_MathF(nameof(digits));
ThrowHelper.ThrowArgumentOutOfRange_RoundingDigits(nameof(digits));
}

if (Abs(x) < singleRoundLimit)
if ((uint)mode > (uint)MidpointRounding.ToPositiveInfinity)
{
float power10 = RoundPower10Single[digits];
x = Round(x * power10, mode) / power10;
ThrowHelper.ThrowArgumentException_InvalidEnumValue(mode);
}

// Rounding to zero fractional digits is just rounding to an integer, which the dedicated
// overload does with a single hardware instruction on most platforms.
if (digits == 0)
{
return Round(x, mode);
}

// Only finite values with a magnitude below the integer boundary can have a fractional
// portion to round. All other values (including NaN and Infinity) are returned unchanged;
// this comparison is naturally false for those cases.
if (Abs(x) < singleIntegerBoundary)
{
// The fast path only handles the small-digit range where `10^digits` is exactly
// representable; larger counts fall back to the exact arbitrary-precision routine.
if ((digits > maxFastRoundingDigits) || !Number.TryRoundToDecimalDigitsFast(x, digits, mode, out float rounded))
{
rounded = Number.RoundToDecimalDigits<float>(x, digits, mode);
}
x = rounded;
}

return x;
Expand Down
Loading
Loading