From 241869259aafbc1e92d5955aaaba6e02eebab2ef Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 23 Jul 2026 15:34:32 -0700 Subject: [PATCH] Optimize Half comparison operators and CompareTo Replace the operator-chained CompareTo with a single NaN check plus a monotonic sign-magnitude ordering key, and add an AVX2 fast path to CompareTo, operator <, and operator <= that compares via (float)Half, which lowers to a hardware vcvtph2ps conversion. The relational operators funnel > and >= through the fast path as well, while the equality operators stay on the cheaper bit logic. Equality is left unchanged since converting to float measured slower there. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib/src/System/Half.cs | 40 +++++++++++++------ .../System.Runtime.Tests/System/HalfTests.cs | 6 +++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs index a07ea3706070aa..40b2587b806b09 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Half.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -8,6 +8,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; namespace System { @@ -157,6 +158,12 @@ internal static ushort ExtractTrailingSignificandFromBits(ushort bits) /// public static bool operator <(Half left, Half right) { + if (Avx2.IsSupported) + { + // (float)Half lowers to a hardware conversion here, so comparing as float is cheaper. + return (float)left < (float)right; + } + if (IsNaN(left) || IsNaN(right)) { // IEEE defines that NaN is unordered with respect to everything, including itself. @@ -185,6 +192,12 @@ internal static ushort ExtractTrailingSignificandFromBits(ushort bits) /// public static bool operator <=(Half left, Half right) { + if (Avx2.IsSupported) + { + // (float)Half lowers to a hardware conversion here, so comparing as float is cheaper. + return (float)left <= (float)right; + } + if (IsNaN(left) || IsNaN(right)) { // IEEE defines that NaN is unordered with respect to everything, including itself. @@ -437,28 +450,31 @@ public int CompareTo(object? obj) /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . public int CompareTo(Half other) { - if (this < other) + if (Avx2.IsSupported) { - return -1; + // (float)Half lowers to a hardware conversion here, so comparing as float is cheaper. + return ((float)this).CompareTo((float)other); } - if (this > other) + if (IsNaN(this)) { - return 1; + return IsNaN(other) ? 0 : -1; } - if (this == other) + if (IsNaN(other)) { - return 0; + return 1; } - if (IsNaN(this)) - { - return IsNaN(other) ? 0 : -1; - } + // Neither value is NaN, so map the sign-magnitude bits to a monotonic ordering. + return GetCompareKey(_value) - GetCompareKey(other._value); + } - Debug.Assert(IsNaN(other)); - return 1; + private static int GetCompareKey(ushort bits) + { + // Positive maps to 0x8000 + bits and negative maps to 0x8000 - magnitude, so both zeros + // collapse to 0x8000 while the ordering stays monotonic across the finite and infinite range. + return ((bits & SignMask) == 0) ? (SignMask + bits) : (SignMask - (bits & ~SignMask)); } /// 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 ee6f0fbbde659e..aa8422565ae2c7 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 @@ -341,6 +341,12 @@ public static IEnumerable CompareTo_TestData() yield return new object[] { (Half)(-180f), (Half)(180f), -1}; yield return new object[] { (Half)(180f), (Half)(-180f), 1}; yield return new object[] { (Half)(-65535), (object)null, 1}; + yield return new object[] { BitConverter.UInt16BitsToHalf(0x0000), BitConverter.UInt16BitsToHalf(0x8000), 0 }; // +0 vs -0 + yield return new object[] { BitConverter.UInt16BitsToHalf(0x8000), BitConverter.UInt16BitsToHalf(0x0000), 0 }; // -0 vs +0 + yield return new object[] { BitConverter.UInt16BitsToHalf(0x8001), Half.Epsilon, -1 }; // -subnormal vs +subnormal + yield return new object[] { Half.NaN, Half.PositiveInfinity, -1 }; + yield return new object[] { Half.PositiveInfinity, Half.NaN, 1 }; + yield return new object[] { Half.NaN, Half.NegativeInfinity, -1 }; } [Theory]