Skip to content

Commit

Permalink
Use generic math for floating point formatting (#102683)
Browse files Browse the repository at this point in the history
* Generic DiyFp

* Generic Grisu3

* Generic Dragon4

* Add MaxRoundTripDigits to MaxPrecisionCustomFormat to FormatInfo

* Generic FormatFloat

* Adapt with existing FP types

* Fix ExtractFractionAndBiasedExponent
  • Loading branch information
huoyaoyuan authored Jun 3, 2024
1 parent 555dde4 commit a4407a1
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 468 deletions.
16 changes: 10 additions & 6 deletions src/libraries/System.Private.CoreLib/src/System/Double.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,33 +354,33 @@ public override int GetHashCode()

public override string ToString()
{
return Number.FormatDouble(m_value, null, NumberFormatInfo.CurrentInfo);
return Number.FormatFloat(m_value, null, NumberFormatInfo.CurrentInfo);
}

public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format)
{
return Number.FormatDouble(m_value, format, NumberFormatInfo.CurrentInfo);
return Number.FormatFloat(m_value, format, NumberFormatInfo.CurrentInfo);
}

public string ToString(IFormatProvider? provider)
{
return Number.FormatDouble(m_value, null, NumberFormatInfo.GetInstance(provider));
return Number.FormatFloat(m_value, null, NumberFormatInfo.GetInstance(provider));
}

public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider)
{
return Number.FormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider));
return Number.FormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider));
}

public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
{
return Number.TryFormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
}

/// <inheritdoc cref="IUtf8SpanFormattable.TryFormat" />
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
{
return Number.TryFormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
}

public static double Parse(string s) => Parse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider: null);
Expand Down Expand Up @@ -2335,6 +2335,10 @@ public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFo

static ulong IBinaryFloatParseAndFormatInfo<double>.FloatToBits(double value) => BitConverter.DoubleToUInt64Bits(value);

static int IBinaryFloatParseAndFormatInfo<double>.MaxRoundTripDigits => 17;

static int IBinaryFloatParseAndFormatInfo<double>.MaxPrecisionCustomFormat => 15;

//
// Helpers
//
Expand Down
16 changes: 10 additions & 6 deletions src/libraries/System.Private.CoreLib/src/System/Half.cs
Original file line number Diff line number Diff line change
Expand Up @@ -505,31 +505,31 @@ public override int GetHashCode()
/// </summary>
public override string ToString()
{
return Number.FormatHalf(this, null, NumberFormatInfo.CurrentInfo);
return Number.FormatFloat(this, null, NumberFormatInfo.CurrentInfo);
}

/// <summary>
/// Returns a string representation of the current value using the specified <paramref name="format"/>.
/// </summary>
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format)
{
return Number.FormatHalf(this, format, NumberFormatInfo.CurrentInfo);
return Number.FormatFloat(this, format, NumberFormatInfo.CurrentInfo);
}

/// <summary>
/// Returns a string representation of the current value with the specified <paramref name="provider"/>.
/// </summary>
public string ToString(IFormatProvider? provider)
{
return Number.FormatHalf(this, null, NumberFormatInfo.GetInstance(provider));
return Number.FormatFloat(this, null, NumberFormatInfo.GetInstance(provider));
}

/// <summary>
/// Returns a string representation of the current value using the specified <paramref name="format"/> and <paramref name="provider"/>.
/// </summary>
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider)
{
return Number.FormatHalf(this, format, NumberFormatInfo.GetInstance(provider));
return Number.FormatFloat(this, format, NumberFormatInfo.GetInstance(provider));
}

/// <summary>
Expand All @@ -542,13 +542,13 @@ public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] strin
/// <returns></returns>
public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
{
return Number.TryFormatHalf(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
return Number.TryFormatFloat(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
}

/// <inheritdoc cref="IUtf8SpanFormattable.TryFormat" />
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
{
return Number.TryFormatHalf(this, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
return Number.TryFormatFloat(this, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
}

//
Expand Down Expand Up @@ -2373,5 +2373,9 @@ public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFo
static Half IBinaryFloatParseAndFormatInfo<Half>.BitsToFloat(ulong bits) => BitConverter.UInt16BitsToHalf((ushort)(bits));

static ulong IBinaryFloatParseAndFormatInfo<Half>.FloatToBits(Half value) => BitConverter.HalfToUInt16Bits(value);

static int IBinaryFloatParseAndFormatInfo<Half>.MaxRoundTripDigits => 5;

static int IBinaryFloatParseAndFormatInfo<Half>.MaxPrecisionCustomFormat => 5;
}
}
63 changes: 10 additions & 53 deletions src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ internal static partial class Number
// DiyFp are not designed to contain special doubles (NaN and Infinity).
internal readonly ref struct DiyFp
{
public const int DoubleImplicitBitIndex = 52;
public const int SingleImplicitBitIndex = 23;
public const int HalfImplicitBitIndex = 10;

public const int SignificandSize = 64;

public readonly ulong f;
Expand All @@ -33,60 +29,21 @@ internal readonly ref struct DiyFp
//
// Precondition:
// The value encoded by value must be greater than 0.
public static DiyFp CreateAndGetBoundaries(double value, out DiyFp mMinus, out DiyFp mPlus)
public static DiyFp CreateAndGetBoundaries<TNumber>(TNumber value, out DiyFp mMinus, out DiyFp mPlus)
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
{
var result = new DiyFp(value);
result.GetBoundaries(DoubleImplicitBitIndex, out mMinus, out mPlus);
var result = Create(value);
result.GetBoundaries(TNumber.DenormalMantissaBits, out mMinus, out mPlus);
return result;
}

// Computes the two boundaries of value.
//
// The bigger boundary (mPlus) is normalized.
// The lower boundary has the same exponent as mPlus.
//
// Precondition:
// The value encoded by value must be greater than 0.
public static DiyFp CreateAndGetBoundaries(float value, out DiyFp mMinus, out DiyFp mPlus)
{
var result = new DiyFp(value);
result.GetBoundaries(SingleImplicitBitIndex, out mMinus, out mPlus);
return result;
}

// Computes the two boundaries of value.
//
// The bigger boundary (mPlus) is normalized.
// The lower boundary has the same exponent as mPlus.
//
// Precondition:
// The value encoded by value must be greater than 0.
public static DiyFp CreateAndGetBoundaries(Half value, out DiyFp mMinus, out DiyFp mPlus)
{
var result = new DiyFp(value);
result.GetBoundaries(HalfImplicitBitIndex, out mMinus, out mPlus);
return result;
}

public DiyFp(double value)
{
Debug.Assert(double.IsFinite(value));
Debug.Assert(value > 0.0);
f = ExtractFractionAndBiasedExponent(value, out e);
}

public DiyFp(float value)
{
Debug.Assert(float.IsFinite(value));
Debug.Assert(value > 0.0f);
f = ExtractFractionAndBiasedExponent(value, out e);
}

public DiyFp(Half value)
public static DiyFp Create<TNumber>(TNumber value)
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
{
Debug.Assert(Half.IsFinite(value));
Debug.Assert((float)value > 0.0f);
f = ExtractFractionAndBiasedExponent(value, out e);
Debug.Assert(TNumber.IsFinite(value));
Debug.Assert(value > TNumber.Zero);
ulong f = ExtractFractionAndBiasedExponent(value, out int e);
return new DiyFp(f, e);
}

public DiyFp(ulong f, int e)
Expand Down
75 changes: 8 additions & 67 deletions src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,82 +10,23 @@ namespace System
// The backing algorithm and the proofs behind it are described in more detail here: https://www.cs.indiana.edu/~dyb/pubs/FP-Printing-PLDI96.pdf
internal static partial class Number
{
public static void Dragon4Double(double value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
public static unsafe void Dragon4<TNumber>(TNumber value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
{
double v = double.IsNegative(value) ? -value : value;
TNumber v = TNumber.IsNegative(value) ? -value : value;

Debug.Assert(v > 0);
Debug.Assert(double.IsFinite(v));
Debug.Assert(v > TNumber.Zero);
Debug.Assert(TNumber.IsFinite(v));

ulong mantissa = ExtractFractionAndBiasedExponent(value, out int exponent);

uint mantissaHighBitIdx;
bool hasUnequalMargins = false;

if ((mantissa >> DiyFp.DoubleImplicitBitIndex) != 0)
if ((mantissa >> TNumber.DenormalMantissaBits) != 0)
{
mantissaHighBitIdx = DiyFp.DoubleImplicitBitIndex;
hasUnequalMargins = (mantissa == (1UL << DiyFp.DoubleImplicitBitIndex));
}
else
{
Debug.Assert(mantissa != 0);
mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa);
}

int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent));

number.Scale = decimalExponent + 1;
number.Digits[length] = (byte)('\0');
number.DigitsCount = length;
}

public static unsafe void Dragon4Half(Half value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
{
Half v = Half.IsNegative(value) ? Half.Negate(value) : value;

Debug.Assert((double)v > 0.0);
Debug.Assert(Half.IsFinite(v));

ushort mantissa = ExtractFractionAndBiasedExponent(value, out int exponent);

uint mantissaHighBitIdx;
bool hasUnequalMargins = false;

if ((mantissa >> DiyFp.HalfImplicitBitIndex) != 0)
{
mantissaHighBitIdx = DiyFp.HalfImplicitBitIndex;
hasUnequalMargins = (mantissa == (1U << DiyFp.HalfImplicitBitIndex));
}
else
{
Debug.Assert(mantissa != 0);
mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa);
}

int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent));

number.Scale = decimalExponent + 1;
number.Digits[length] = (byte)('\0');
number.DigitsCount = length;
}

public static unsafe void Dragon4Single(float value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
{
float v = float.IsNegative(value) ? -value : value;

Debug.Assert(v > 0);
Debug.Assert(float.IsFinite(v));

uint mantissa = ExtractFractionAndBiasedExponent(value, out int exponent);

uint mantissaHighBitIdx;
bool hasUnequalMargins = false;

if ((mantissa >> DiyFp.SingleImplicitBitIndex) != 0)
{
mantissaHighBitIdx = DiyFp.SingleImplicitBitIndex;
hasUnequalMargins = (mantissa == (1U << DiyFp.SingleImplicitBitIndex));
mantissaHighBitIdx = TNumber.DenormalMantissaBits;
hasUnequalMargins = (mantissa == (1U << TNumber.DenormalMantissaBits));
}
else
{
Expand Down
Loading

0 comments on commit a4407a1

Please sign in to comment.