Skip to content

Commit b0f9714

Browse files
author
Prashanth Govindarajan
authored
Half: An IEEE 754 compliant float16 type (#37630) (#38416)
Fixes #936. A lot of this code is a port of what have in [corefxlab](https://github.com/dotnet/corefxlab/tree/master/src/System.Numerics.Experimental).
1 parent 7808f2d commit b0f9714

File tree

14 files changed

+2017
-4
lines changed

14 files changed

+2017
-4
lines changed

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3751,4 +3751,7 @@
37513751
<data name="IDynamicInterfaceCastable_NotInterface" xml:space="preserve">
37523752
<value>Type '{0}' returned by IDynamicInterfaceCastable is not an interface.</value>
37533753
</data>
3754+
<data name="Arg_MustBeHalf" xml:space="preserve">
3755+
<value>Object must be of type Half.</value>
3756+
</data>
37543757
</root>

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@
338338
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\UmAlQuraCalendar.cs" />
339339
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\UnicodeCategory.cs" />
340340
<Compile Include="$(MSBuildThisFileDirectory)System\Guid.cs" />
341+
<Compile Include="$(MSBuildThisFileDirectory)System\Half.cs" />
341342
<Compile Include="$(MSBuildThisFileDirectory)System\HashCode.cs" />
342343
<Compile Include="$(MSBuildThisFileDirectory)System\IAsyncDisposable.cs" />
343344
<Compile Include="$(MSBuildThisFileDirectory)System\IAsyncResult.cs" />

src/libraries/System.Private.CoreLib/src/System/BitConverter.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,5 +499,17 @@ public static unsafe float Int32BitsToSingle(int value)
499499

500500
return *((float*)&value);
501501
}
502+
503+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
504+
internal static unsafe short HalfToInt16Bits(Half value)
505+
{
506+
return *((short*)&value);
507+
}
508+
509+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
510+
internal static unsafe Half Int16BitsToHalf(short value)
511+
{
512+
return *(Half*)&value;
513+
}
502514
}
503515
}

src/libraries/System.Private.CoreLib/src/System/Half.cs

Lines changed: 692 additions & 0 deletions
Large diffs are not rendered by default.

src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal readonly ref struct DiyFp
2020
{
2121
public const int DoubleImplicitBitIndex = 52;
2222
public const int SingleImplicitBitIndex = 23;
23+
public const int HalfImplicitBitIndex = 10;
2324

2425
public const int SignificandSize = 64;
2526

@@ -54,6 +55,20 @@ public static DiyFp CreateAndGetBoundaries(float value, out DiyFp mMinus, out Di
5455
return result;
5556
}
5657

58+
// Computes the two boundaries of value.
59+
//
60+
// The bigger boundary (mPlus) is normalized.
61+
// The lower boundary has the same exponent as mPlus.
62+
//
63+
// Precondition:
64+
// The value encoded by value must be greater than 0.
65+
public static DiyFp CreateAndGetBoundaries(Half value, out DiyFp mMinus, out DiyFp mPlus)
66+
{
67+
var result = new DiyFp(value);
68+
result.GetBoundaries(HalfImplicitBitIndex, out mMinus, out mPlus);
69+
return result;
70+
}
71+
5772
public DiyFp(double value)
5873
{
5974
Debug.Assert(double.IsFinite(value));
@@ -68,6 +83,13 @@ public DiyFp(float value)
6883
f = ExtractFractionAndBiasedExponent(value, out e);
6984
}
7085

86+
public DiyFp(Half value)
87+
{
88+
Debug.Assert(Half.IsFinite(value));
89+
Debug.Assert((float)value > 0.0f);
90+
f = ExtractFractionAndBiasedExponent(value, out e);
91+
}
92+
7193
public DiyFp(ulong f, int e)
7294
{
7395
this.f = f;

src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,36 @@ public static void Dragon4Double(double value, int cutoffNumber, bool isSignific
4141
number.DigitsCount = length;
4242
}
4343

44+
public static unsafe void Dragon4Half(Half value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
45+
{
46+
Half v = Half.IsNegative(value) ? Half.Negate(value) : value;
47+
48+
Debug.Assert((double)v > 0.0);
49+
Debug.Assert(Half.IsFinite(v));
50+
51+
ushort mantissa = ExtractFractionAndBiasedExponent(value, out int exponent);
52+
53+
uint mantissaHighBitIdx;
54+
bool hasUnequalMargins = false;
55+
56+
if ((mantissa >> DiyFp.HalfImplicitBitIndex) != 0)
57+
{
58+
mantissaHighBitIdx = DiyFp.HalfImplicitBitIndex;
59+
hasUnequalMargins = (mantissa == (1U << DiyFp.HalfImplicitBitIndex));
60+
}
61+
else
62+
{
63+
Debug.Assert(mantissa != 0);
64+
mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa);
65+
}
66+
67+
int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent));
68+
69+
number.Scale = decimalExponent + 1;
70+
number.Digits[length] = (byte)('\0');
71+
number.DigitsCount = length;
72+
}
73+
4474
public static unsafe void Dragon4Single(float value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
4575
{
4676
float v = float.IsNegative(value) ? -value : value;

src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ internal static partial class Number
247247
// SinglePrecision and DoublePrecision represent the maximum number of digits required
248248
// to guarantee that any given Single or Double can roundtrip. Some numbers may require
249249
// less, but none will require more.
250+
private const int HalfPrecision = 5;
250251
private const int SinglePrecision = 9;
251252
private const int DoublePrecision = 17;
252253

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

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

634-
if ((value != 0.0f) && (!isSignificantDigits || !Grisu3.TryRunSingle(value, precision, ref number)))
636+
if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunSingle(value, precision, ref number)))
635637
{
636638
Dragon4Single(value, precision, isSignificantDigits, ref number);
637639
}
@@ -668,6 +670,91 @@ public static bool TryFormatSingle(float value, ReadOnlySpan<char> format, Numbe
668670
return null;
669671
}
670672

673+
public static string FormatHalf(Half value, string? format, NumberFormatInfo info)
674+
{
675+
var sb = new ValueStringBuilder(stackalloc char[CharStackBufferSize]);
676+
return FormatHalf(ref sb, value, format, info) ?? sb.ToString();
677+
}
678+
679+
/// <summary>Formats the specified value according to the specified format and info.</summary>
680+
/// <returns>
681+
/// Non-null if an existing string can be returned, in which case the builder will be unmodified.
682+
/// Null if no existing string was returned, in which case the formatted output is in the builder.
683+
/// </returns>
684+
private static unsafe string? FormatHalf(ref ValueStringBuilder sb, Half value, ReadOnlySpan<char> format, NumberFormatInfo info)
685+
{
686+
if (!Half.IsFinite(value))
687+
{
688+
if (Half.IsNaN(value))
689+
{
690+
return info.NaNSymbol;
691+
}
692+
693+
return Half.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol;
694+
}
695+
696+
char fmt = ParseFormatSpecifier(format, out int precision);
697+
byte* pDigits = stackalloc byte[HalfNumberBufferLength];
698+
699+
if (fmt == '\0')
700+
{
701+
precision = HalfPrecisionCustomFormat;
702+
}
703+
704+
NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, HalfNumberBufferLength);
705+
number.IsNegative = Half.IsNegative(value);
706+
707+
// We need to track the original precision requested since some formats
708+
// accept values like 0 and others may require additional fixups.
709+
int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits);
710+
711+
if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunHalf(value, precision, ref number)))
712+
{
713+
Dragon4Half(value, precision, isSignificantDigits, ref number);
714+
}
715+
716+
number.CheckConsistency();
717+
718+
// When the number is known to be roundtrippable (either because we requested it be, or
719+
// because we know we have enough digits to satisfy roundtrippability), we should validate
720+
// that the number actually roundtrips back to the original result.
721+
722+
Debug.Assert(((precision != -1) && (precision < HalfPrecision)) || (BitConverter.HalfToInt16Bits(value) == BitConverter.HalfToInt16Bits(NumberToHalf(ref number))));
723+
724+
if (fmt != 0)
725+
{
726+
if (precision == -1)
727+
{
728+
Debug.Assert((fmt == 'G') || (fmt == 'g') || (fmt == 'R') || (fmt == 'r'));
729+
730+
// For the roundtrip and general format specifiers, when returning the shortest roundtrippable
731+
// string, we need to update the maximum number of digits to be the greater of number.DigitsCount
732+
// or SinglePrecision. This ensures that we continue returning "pretty" strings for values with
733+
// less digits. One example this fixes is "-60", which would otherwise be formatted as "-6E+01"
734+
// since DigitsCount would be 1 and the formatter would almost immediately switch to scientific notation.
735+
736+
nMaxDigits = Math.Max(number.DigitsCount, HalfPrecision);
737+
}
738+
NumberToString(ref sb, ref number, fmt, nMaxDigits, info);
739+
}
740+
else
741+
{
742+
Debug.Assert(precision == HalfPrecisionCustomFormat);
743+
NumberToStringFormat(ref sb, ref number, format, info);
744+
}
745+
return null;
746+
}
747+
748+
public static bool TryFormatHalf(Half value, ReadOnlySpan<char> format, NumberFormatInfo info, Span<char> destination, out int charsWritten)
749+
{
750+
var sb = new ValueStringBuilder(stackalloc char[CharStackBufferSize]);
751+
string? s = FormatHalf(ref sb, value, format, info);
752+
return s != null ?
753+
TryCopyTo(s, destination, out charsWritten) :
754+
sb.TryCopyTo(destination, out charsWritten);
755+
}
756+
757+
671758
private static bool TryCopyTo(string source, Span<char> destination, out int charsWritten)
672759
{
673760
Debug.Assert(source != null);
@@ -2563,6 +2650,38 @@ private static ulong ExtractFractionAndBiasedExponent(double value, out int expo
25632650
return fraction;
25642651
}
25652652

2653+
private static ushort ExtractFractionAndBiasedExponent(Half value, out int exponent)
2654+
{
2655+
ushort bits = (ushort)BitConverter.HalfToInt16Bits(value);
2656+
ushort fraction = (ushort)(bits & 0x3FF);
2657+
exponent = ((int)(bits >> 10) & 0x1F);
2658+
2659+
if (exponent != 0)
2660+
{
2661+
// For normalized value, according to https://en.wikipedia.org/wiki/Half-precision_floating-point_format
2662+
// value = 1.fraction * 2^(exp - 15)
2663+
// = (1 + mantissa / 2^10) * 2^(exp - 15)
2664+
// = (2^10 + mantissa) * 2^(exp - 15 - 10)
2665+
//
2666+
// So f = (2^10 + mantissa), e = exp - 25;
2667+
2668+
fraction |= (ushort)(1U << 10);
2669+
exponent -= 25;
2670+
}
2671+
else
2672+
{
2673+
// For denormalized value, according to https://en.wikipedia.org/wiki/Half-precision_floating-point_format
2674+
// value = 0.fraction * 2^(1 - 15)
2675+
// = (mantissa / 2^10) * 2^(-14)
2676+
// = mantissa * 2^(-14 - 10)
2677+
// = mantissa * 2^(-24)
2678+
// So f = mantissa, e = -24
2679+
exponent = -24;
2680+
}
2681+
2682+
return fraction;
2683+
}
2684+
25662685
private static uint ExtractFractionAndBiasedExponent(float value, out int exponent)
25672686
{
25682687
uint bits = (uint)(BitConverter.SingleToInt32Bits(value));

src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,40 @@ public static bool TryRunDouble(double value, int requestedDigits, ref NumberBuf
356356
return result;
357357
}
358358

359+
public static bool TryRunHalf(Half value, int requestedDigits, ref NumberBuffer number)
360+
{
361+
Half v = Half.IsNegative(value) ? Half.Negate(value) : value;
362+
363+
Debug.Assert((double)v > 0);
364+
Debug.Assert(Half.IsFinite(v));
365+
366+
int length;
367+
int decimalExponent;
368+
bool result;
369+
370+
if (requestedDigits == -1)
371+
{
372+
DiyFp w = DiyFp.CreateAndGetBoundaries(v, out DiyFp boundaryMinus, out DiyFp boundaryPlus).Normalize();
373+
result = TryRunShortest(in boundaryMinus, in w, in boundaryPlus, number.Digits, out length, out decimalExponent);
374+
}
375+
else
376+
{
377+
DiyFp w = new DiyFp(v).Normalize();
378+
result = TryRunCounted(in w, requestedDigits, number.Digits, out length, out decimalExponent);
379+
}
380+
381+
if (result)
382+
{
383+
Debug.Assert((requestedDigits == -1) || (length == requestedDigits));
384+
385+
number.Scale = length + decimalExponent;
386+
number.Digits[length] = (byte)('\0');
387+
number.DigitsCount = length;
388+
}
389+
390+
return result;
391+
}
392+
359393
public static bool TryRunSingle(float value, int requestedDigits, ref NumberBuffer number)
360394
{
361395
float v = float.IsNegative(value) ? -value : value;

src/libraries/System.Private.CoreLib/src/System/Number.NumberBuffer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal static partial class Number
1616
internal const int Int32NumberBufferLength = 10 + 1; // 10 for the longest input: 2,147,483,647
1717
internal const int Int64NumberBufferLength = 19 + 1; // 19 for the longest input: 9,223,372,036,854,775,807
1818
internal const int SingleNumberBufferLength = 112 + 1 + 1; // 112 for the longest input + 1 for rounding: 1.40129846E-45
19+
internal const int HalfNumberBufferLength = 21; // 19 for the longest input + 1 for rounding (+1 for the null terminator)
1920
internal const int UInt32NumberBufferLength = 10 + 1; // 10 for the longest input: 4,294,967,295
2021
internal const int UInt64NumberBufferLength = 20 + 1; // 20 for the longest input: 18,446,744,073,709,551,615
2122

src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ public readonly struct FloatingPointInfo
2626
infinityBits: 0x7F800000
2727
);
2828

29+
public static readonly FloatingPointInfo Half = new FloatingPointInfo(
30+
denormalMantissaBits: 10,
31+
exponentBits: 5,
32+
maxBinaryExponent: 15,
33+
exponentBias: 15,
34+
infinityBits: 0x7C00
35+
);
36+
2937
public ulong ZeroBits { get; }
3038
public ulong InfinityBits { get; }
3139

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

366374
byte* src = number.GetDigitsPointer();
367375

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

394+
if (info.DenormalMantissaBits == 10)
395+
{
396+
return (ushort)(BitConverter.HalfToInt16Bits((Half)result));
397+
}
386398
return (uint)(BitConverter.SingleToInt32Bits(result));
387399
}
388400

@@ -404,11 +416,15 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number, in Float
404416
{
405417
return (ulong)(BitConverter.DoubleToInt64Bits(result));
406418
}
407-
else
419+
else if (info.DenormalMantissaBits == 23)
408420
{
409-
Debug.Assert(info.DenormalMantissaBits == 23);
410421
return (uint)(BitConverter.SingleToInt32Bits((float)(result)));
411422
}
423+
else
424+
{
425+
Debug.Assert(info.DenormalMantissaBits == 10);
426+
return (uint)(BitConverter.HalfToInt16Bits((Half)(result)));
427+
}
412428
}
413429

414430
return NumberToFloatingPointBitsSlow(ref number, in info, positiveExponent, integerDigitsPresent, fractionalDigitsPresent);

0 commit comments

Comments
 (0)