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 @@ -3751,4 +3751,7 @@
<data name="IDynamicInterfaceCastable_NotInterface" xml:space="preserve">
<value>Type '{0}' returned by IDynamicInterfaceCastable is not an interface.</value>
</data>
<data name="Arg_MustBeHalf" xml:space="preserve">
<value>Object must be of type Half.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\UmAlQuraCalendar.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\UnicodeCategory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Guid.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Half.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\HashCode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IAsyncDisposable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IAsyncResult.cs" />
Expand Down
12 changes: 12 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/BitConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,5 +499,17 @@ public static unsafe float Int32BitsToSingle(int value)

return *((float*)&value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe short HalfToInt16Bits(Half value)
{
return *((short*)&value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe Half Int16BitsToHalf(short value)
{
return *(Half*)&value;
}
}
}
692 changes: 692 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Half.cs

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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;

Expand Down Expand Up @@ -54,6 +55,20 @@ public static DiyFp CreateAndGetBoundaries(float value, out DiyFp mMinus, out Di
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));
Expand All @@ -68,6 +83,13 @@ public DiyFp(float value)
f = ExtractFractionAndBiasedExponent(value, out e);
}

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

public DiyFp(ulong f, int e)
{
this.f = f;
Expand Down
30 changes: 30 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ public static void Dragon4Double(double value, int cutoffNumber, bool isSignific
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;
Expand Down
121 changes: 120 additions & 1 deletion src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ internal static partial class Number
// SinglePrecision and DoublePrecision represent the maximum number of digits required
// to guarantee that any given Single or Double can roundtrip. Some numbers may require
// less, but none will require more.
private const int HalfPrecision = 5;
private const int SinglePrecision = 9;
private const int DoublePrecision = 17;

Expand All @@ -256,6 +257,7 @@ internal static partial class Number
// In order to support more digits, we would need to update ParseFormatSpecifier to pre-parse
// the format and determine exactly how many digits are being requested and whether they
// represent "significant digits" or "digits after the decimal point".
private const int HalfPrecisionCustomFormat = 5;
private const int SinglePrecisionCustomFormat = 7;
private const int DoublePrecisionCustomFormat = 15;

Expand Down Expand Up @@ -631,7 +633,7 @@ public static bool TryFormatSingle(float value, ReadOnlySpan<char> format, Numbe
// accept values like 0 and others may require additional fixups.
int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits);

if ((value != 0.0f) && (!isSignificantDigits || !Grisu3.TryRunSingle(value, precision, ref number)))
if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunSingle(value, precision, ref number)))
{
Dragon4Single(value, precision, isSignificantDigits, ref number);
}
Expand Down Expand Up @@ -668,6 +670,91 @@ public static bool TryFormatSingle(float value, ReadOnlySpan<char> format, Numbe
return null;
}

public static string FormatHalf(Half value, string? format, NumberFormatInfo info)
{
var sb = new ValueStringBuilder(stackalloc char[CharStackBufferSize]);
return FormatHalf(ref sb, value, format, info) ?? sb.ToString();
}

/// <summary>Formats the specified value according to the specified format and info.</summary>
/// <returns>
/// Non-null if an existing string can be returned, in which case the builder will be unmodified.
/// Null if no existing string was returned, in which case the formatted output is in the builder.
/// </returns>
private static unsafe string? FormatHalf(ref ValueStringBuilder sb, Half value, ReadOnlySpan<char> format, NumberFormatInfo info)
{
if (!Half.IsFinite(value))
{
if (Half.IsNaN(value))
{
return info.NaNSymbol;
}

return Half.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol;
}

char fmt = ParseFormatSpecifier(format, out int precision);
byte* pDigits = stackalloc byte[HalfNumberBufferLength];

if (fmt == '\0')
{
precision = HalfPrecisionCustomFormat;
}

NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, HalfNumberBufferLength);
number.IsNegative = Half.IsNegative(value);

// We need to track the original precision requested since some formats
// accept values like 0 and others may require additional fixups.
int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits);

if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunHalf(value, precision, ref number)))
{
Dragon4Half(value, precision, isSignificantDigits, ref number);
}

number.CheckConsistency();

// When the number is known to be roundtrippable (either because we requested it be, or
// because we know we have enough digits to satisfy roundtrippability), we should validate
// that the number actually roundtrips back to the original result.

Debug.Assert(((precision != -1) && (precision < HalfPrecision)) || (BitConverter.HalfToInt16Bits(value) == BitConverter.HalfToInt16Bits(NumberToHalf(ref number))));

if (fmt != 0)
{
if (precision == -1)
{
Debug.Assert((fmt == 'G') || (fmt == 'g') || (fmt == 'R') || (fmt == 'r'));

// For the roundtrip and general format specifiers, when returning the shortest roundtrippable
// string, we need to update the maximum number of digits to be the greater of number.DigitsCount
// or SinglePrecision. This ensures that we continue returning "pretty" strings for values with
// less digits. One example this fixes is "-60", which would otherwise be formatted as "-6E+01"
// since DigitsCount would be 1 and the formatter would almost immediately switch to scientific notation.

nMaxDigits = Math.Max(number.DigitsCount, HalfPrecision);
}
NumberToString(ref sb, ref number, fmt, nMaxDigits, info);
}
else
{
Debug.Assert(precision == HalfPrecisionCustomFormat);
NumberToStringFormat(ref sb, ref number, format, info);
}
return null;
}

public static bool TryFormatHalf(Half value, ReadOnlySpan<char> format, NumberFormatInfo info, Span<char> destination, out int charsWritten)
{
var sb = new ValueStringBuilder(stackalloc char[CharStackBufferSize]);
string? s = FormatHalf(ref sb, value, format, info);
return s != null ?
TryCopyTo(s, destination, out charsWritten) :
sb.TryCopyTo(destination, out charsWritten);
}


private static bool TryCopyTo(string source, Span<char> destination, out int charsWritten)
{
Debug.Assert(source != null);
Expand Down Expand Up @@ -2563,6 +2650,38 @@ private static ulong ExtractFractionAndBiasedExponent(double value, out int expo
return fraction;
}

private static ushort ExtractFractionAndBiasedExponent(Half value, out int exponent)
{
ushort bits = (ushort)BitConverter.HalfToInt16Bits(value);
ushort fraction = (ushort)(bits & 0x3FF);
exponent = ((int)(bits >> 10) & 0x1F);

if (exponent != 0)
{
// For normalized value, according to https://en.wikipedia.org/wiki/Half-precision_floating-point_format
// value = 1.fraction * 2^(exp - 15)
// = (1 + mantissa / 2^10) * 2^(exp - 15)
// = (2^10 + mantissa) * 2^(exp - 15 - 10)
//
// So f = (2^10 + mantissa), e = exp - 25;

fraction |= (ushort)(1U << 10);
exponent -= 25;
}
else
{
// For denormalized value, according to https://en.wikipedia.org/wiki/Half-precision_floating-point_format
// value = 0.fraction * 2^(1 - 15)
// = (mantissa / 2^10) * 2^(-14)
// = mantissa * 2^(-14 - 10)
// = mantissa * 2^(-24)
// So f = mantissa, e = -24
exponent = -24;
}

return fraction;
}

private static uint ExtractFractionAndBiasedExponent(float value, out int exponent)
{
uint bits = (uint)(BitConverter.SingleToInt32Bits(value));
Expand Down
34 changes: 34 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,40 @@ public static bool TryRunDouble(double value, int requestedDigits, ref NumberBuf
return result;
}

public static bool TryRunHalf(Half value, int requestedDigits, ref NumberBuffer number)
{
Half v = Half.IsNegative(value) ? Half.Negate(value) : value;

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

int length;
int decimalExponent;
bool result;

if (requestedDigits == -1)
{
DiyFp w = DiyFp.CreateAndGetBoundaries(v, out DiyFp boundaryMinus, out DiyFp boundaryPlus).Normalize();
result = TryRunShortest(in boundaryMinus, in w, in boundaryPlus, number.Digits, out length, out decimalExponent);
}
else
{
DiyFp w = new DiyFp(v).Normalize();
result = TryRunCounted(in w, requestedDigits, number.Digits, out length, out decimalExponent);
}

if (result)
{
Debug.Assert((requestedDigits == -1) || (length == requestedDigits));

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

return result;
}

public static bool TryRunSingle(float value, int requestedDigits, ref NumberBuffer number)
{
float v = float.IsNegative(value) ? -value : value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal static partial class Number
internal const int Int32NumberBufferLength = 10 + 1; // 10 for the longest input: 2,147,483,647
internal const int Int64NumberBufferLength = 19 + 1; // 19 for the longest input: 9,223,372,036,854,775,807
internal const int SingleNumberBufferLength = 112 + 1 + 1; // 112 for the longest input + 1 for rounding: 1.40129846E-45
internal const int HalfNumberBufferLength = 21; // 19 for the longest input + 1 for rounding (+1 for the null terminator)
internal const int UInt32NumberBufferLength = 10 + 1; // 10 for the longest input: 4,294,967,295
internal const int UInt64NumberBufferLength = 20 + 1; // 20 for the longest input: 18,446,744,073,709,551,615

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ public readonly struct FloatingPointInfo
infinityBits: 0x7F800000
);

public static readonly FloatingPointInfo Half = new FloatingPointInfo(
denormalMantissaBits: 10,
exponentBits: 5,
maxBinaryExponent: 15,
exponentBias: 15,
infinityBits: 0x7C00
);

public ulong ZeroBits { get; }
public ulong InfinityBits { get; }

Expand Down Expand Up @@ -365,7 +373,7 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number, in Float

byte* src = number.GetDigitsPointer();

if ((info.DenormalMantissaBits == 23) && (totalDigits <= 7) && (fastExponent <= 10))
if ((info.DenormalMantissaBits <= 23) && (totalDigits <= 7) && (fastExponent <= 10))
{
// It is only valid to do this optimization for single-precision floating-point
// values since we can lose some of the mantissa bits and would return the
Expand All @@ -383,6 +391,10 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number, in Float
result *= scale;
}

if (info.DenormalMantissaBits == 10)
{
return (ushort)(BitConverter.HalfToInt16Bits((Half)result));
}
return (uint)(BitConverter.SingleToInt32Bits(result));
}

Expand All @@ -404,11 +416,15 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number, in Float
{
return (ulong)(BitConverter.DoubleToInt64Bits(result));
}
else
else if (info.DenormalMantissaBits == 23)
{
Debug.Assert(info.DenormalMantissaBits == 23);
return (uint)(BitConverter.SingleToInt32Bits((float)(result)));
}
else
{
Debug.Assert(info.DenormalMantissaBits == 10);
return (uint)(BitConverter.HalfToInt16Bits((Half)(result)));
}
}

return NumberToFloatingPointBitsSlow(ref number, in info, positiveExponent, integerDigitsPresent, fractionalDigitsPresent);
Expand Down
Loading