-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Implement IEEE754 totalOrder comparer #75517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
ca008c0
Basic implementation of total order
huoyaoyuan 0c5f1a3
Add public api reference
huoyaoyuan 5de64d0
Add basic tests
huoyaoyuan 915924c
Apply suggestions from code review
huoyaoyuan 57d1304
Update xmldoc
huoyaoyuan 7c321da
Update comparison logic to save calls
huoyaoyuan a7ae51c
Handle order of NaN
huoyaoyuan 4f03b0b
Implement integer comparison semantic
huoyaoyuan 83afc06
Change NaN semantic in fallback comparison
huoyaoyuan 46607fc
Adjust test data
huoyaoyuan 86e1706
Change to struct
huoyaoyuan 3a95831
Fix ApiCompat
huoyaoyuan c60098b
Manually apply suggestion from review
huoyaoyuan b9f56d4
Defensive for custom float-point
huoyaoyuan fb5788e
Nit
huoyaoyuan 6be2157
Merge branch 'main' into total-order
huoyaoyuan 8e8ec37
Fix comments
huoyaoyuan 2116b16
Implement Equals and GetHashCode
huoyaoyuan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
src/libraries/System.Private.CoreLib/src/System/Numerics/TotalOrderIeee754Comparer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace System.Numerics | ||
{ | ||
/// <summary> | ||
/// Represents a comparison operation that compares floating-point numbers | ||
/// with IEEE 754 totalOrder semantic. | ||
/// </summary> | ||
/// <typeparam name="T">The type of the numbers to be compared, must be an IEEE 754 floating-point type.</typeparam> | ||
public readonly struct TotalOrderIeee754Comparer<T> : IComparer<T>, IEqualityComparer<T>, IEquatable<TotalOrderIeee754Comparer<T>> | ||
where T : IFloatingPointIeee754<T>? | ||
{ | ||
/// <summary> | ||
/// Compares two numbers with IEEE 754 totalOrder semantic and returns | ||
/// a value indicating whether one is less than, equal to, or greater than the other. | ||
/// </summary> | ||
/// <param name="x">The first number to compare.</param> | ||
/// <param name="y">The second number to compare.</param> | ||
/// <returns> | ||
/// A signed integer that indicates the relative | ||
/// values of <paramref name="x"/> and <paramref name="y"/>, as shown in the following table. | ||
/// <list type="table"> | ||
/// <listheader> | ||
/// <term> Value</term> | ||
/// <description> Meaning</description> | ||
/// </listheader> | ||
/// <item> | ||
/// <term> Less than zero</term> | ||
/// <description><paramref name = "x" /> is less than <paramref name="y" /></description> | ||
/// </item> | ||
/// <item> | ||
/// <term> Zero</term> | ||
/// <description><paramref name = "x" /> equals <paramref name="y" /></description> | ||
/// </item> | ||
/// <item> | ||
/// <term> Greater than zero</term> | ||
/// <description><paramref name = "x" /> is greater than <paramref name="y" /></description> | ||
/// </item> | ||
/// </list> | ||
/// </returns> | ||
/// <remarks> | ||
/// IEEE 754 specification defines totalOrder as <= semantic. | ||
/// totalOrder(x,y) is <see langword="true"/> when the result of this method is less than or equal to 0. | ||
/// </remarks> | ||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public int Compare(T? x, T? y) | ||
tannergooding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (typeof(T) == typeof(float)) | ||
{ | ||
return CompareIntegerSemantic(BitConverter.SingleToInt32Bits((float)(object)x!), BitConverter.SingleToInt32Bits((float)(object)y!)); | ||
} | ||
else if (typeof(T) == typeof(double)) | ||
{ | ||
return CompareIntegerSemantic(BitConverter.DoubleToInt64Bits((double)(object)x!), BitConverter.DoubleToInt64Bits((double)(object)y!)); | ||
} | ||
else if (typeof(T) == typeof(Half)) | ||
{ | ||
return CompareIntegerSemantic(BitConverter.HalfToInt16Bits((Half)(object)x!), BitConverter.HalfToInt16Bits((Half)(object)y!)); | ||
} | ||
else | ||
{ | ||
return CompareGeneric(x, y); | ||
} | ||
|
||
static int CompareIntegerSemantic<TInteger>(TInteger x, TInteger y) | ||
where TInteger : struct, IBinaryInteger<TInteger>, ISignedNumber<TInteger> | ||
{ | ||
// In IEEE 754 binary floating-point representation, a number is represented as Sign|Exponent|Significand | ||
// Normal numbers has an implicit 1. in front of the significand, so value with larger exponent will have larger absolute value | ||
// Inf and NaN are defined as Exponent=All 1s, while Inf has Significand=0, sNaN has Significand=0xxx and qNaN has Significand=1xxx | ||
// This also satisfies totalOrder definition which is +x < +Inf < +sNaN < +qNaN | ||
|
||
// The order of NaNs of same category and same sign is implementation defined, | ||
// here we define it as the order of exponent bits to simplify comparison | ||
|
||
// Negative values are represented in sign-magnitude, instead of two's complement like integers | ||
// Just negating the comparison result when both numbers are negative is enough | ||
|
||
return (TInteger.IsNegative(x) && TInteger.IsNegative(y)) ? y.CompareTo(x) : x.CompareTo(y); | ||
} | ||
|
||
static int CompareGeneric(T? x, T? y) | ||
{ | ||
// IComparer contract is null < value | ||
|
||
if (x is null) | ||
{ | ||
return (y is null) ? 0 : -1; | ||
} | ||
else if (y is null) | ||
{ | ||
return 1; | ||
} | ||
|
||
// If < or > returns true, the result satisfies definition of totalOrder too | ||
|
||
if (x < y) | ||
{ | ||
return -1; | ||
} | ||
else if (x > y) | ||
{ | ||
return 1; | ||
} | ||
else if (x == y) | ||
{ | ||
if (T.IsZero(x)) // only zeros are equal to zeros | ||
{ | ||
// IEEE 754 numbers are either positive or negative. Skip check for the opposite. | ||
|
||
if (T.IsNegative(x)) | ||
{ | ||
return T.IsNegative(y) ? 0 : -1; | ||
} | ||
else | ||
{ | ||
return T.IsPositive(y) ? 0 : 1; | ||
} | ||
} | ||
else | ||
{ | ||
// Equivalant values are compared by their exponent parts, | ||
// and the value with smaller exponent is considered closer to zero. | ||
|
||
// This only applies to IEEE 754 decimals. Consider to add support if decimals are added into .NET. | ||
return 0; | ||
} | ||
} | ||
else | ||
{ | ||
tannergooding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// One or two of the values are NaN | ||
// totalOrder defines that -qNaN < -sNaN < x < +sNaN < + qNaN | ||
|
||
static int CompareSignificand(T x, T y) | ||
{ | ||
// IEEE 754 totalOrder only defines the order of NaN type bit (the first bit of significand) | ||
// To match the integer semantic comparison above, here we compare all the significand bits | ||
// Revisit this if decimals are added | ||
|
||
// Leave the space for custom floating-point type that has variable significand length | ||
|
||
int xSignificandBits = x!.GetSignificandBitLength(); | ||
int ySignificandBits = y!.GetSignificandBitLength(); | ||
|
||
if (xSignificandBits == ySignificandBits) | ||
{ | ||
// Prevent stack overflow for huge numbers | ||
const int StackAllocThreshold = 256; | ||
|
||
int xSignificandLength = x.GetSignificandByteCount(); | ||
int ySignificandLength = y.GetSignificandByteCount(); | ||
|
||
Span<byte> significandX = xSignificandLength <= StackAllocThreshold ? stackalloc byte[xSignificandLength] : new byte[xSignificandLength]; | ||
Span<byte> significandY = ySignificandLength <= StackAllocThreshold ? stackalloc byte[ySignificandLength] : new byte[ySignificandLength]; | ||
|
||
x.WriteSignificandBigEndian(significandX); | ||
y.WriteSignificandBigEndian(significandY); | ||
|
||
return significandX.SequenceCompareTo(significandY); | ||
} | ||
else | ||
{ | ||
return xSignificandBits.CompareTo(ySignificandBits); | ||
} | ||
} | ||
|
||
if (T.IsNaN(x)) | ||
{ | ||
if (T.IsNaN(y)) | ||
{ | ||
if (T.IsNegative(x)) | ||
{ | ||
return T.IsPositive(y) ? -1 : CompareSignificand(y, x); | ||
} | ||
else | ||
{ | ||
return T.IsNegative(y) ? 1 : CompareSignificand(x, y); | ||
} | ||
} | ||
else | ||
{ | ||
return T.IsPositive(x) ? 1 : -1; | ||
} | ||
} | ||
else if (T.IsNaN(y)) | ||
{ | ||
return T.IsPositive(y) ? -1 : 1; | ||
} | ||
else | ||
{ | ||
// T does not correctly implement IEEE754 semantics | ||
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidArgumentForComparison); | ||
return 0; // unreachable | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Determines whether the specified numbers are equal. | ||
/// </summary> | ||
/// <param name="x">The first number of type <typeparamref name="T"/> to compare.</param> | ||
/// <param name="y">The second number of type <typeparamref name="T"/> to compare.</param> | ||
/// <returns><see langword="true"/> if the specified numbers are equal; otherwise, <see langword="false"/>.</returns> | ||
/// <remarks> | ||
/// There is no corresponding equals semantic with totalOrder defined by IEEE 754 specification. | ||
/// This method returns <see langword="true"/> when <see cref="Compare(T?, T?)"/> returns 0. | ||
/// </remarks> | ||
public bool Equals(T? x, T? y) => Compare(x, y) == 0; | ||
|
||
/// <summary> | ||
/// Returns a hash code for the specified number. | ||
/// </summary> | ||
/// <param name="obj">The number for which a hash code is to be returned.</param> | ||
/// <returns>A hash code for the specified number.</returns> | ||
public int GetHashCode([DisallowNull] T obj) | ||
{ | ||
ArgumentNullException.ThrowIfNull(obj, nameof(obj)); | ||
return obj.GetHashCode(); | ||
} | ||
|
||
public bool Equals(TotalOrderIeee754Comparer<T> other) => true; | ||
|
||
public override bool Equals([NotNullWhen(true)] object? obj) => obj is TotalOrderIeee754Comparer<T>; | ||
|
||
public override int GetHashCode() => EqualityComparer<T>.Default.GetHashCode(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
src/libraries/System.Runtime/tests/System/Numerics/TotalOrderIeee754ComparerTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Collections.Generic; | ||
using System.Numerics; | ||
using System.Runtime.InteropServices; | ||
using Xunit; | ||
|
||
namespace System.Runtime.Tests | ||
{ | ||
public sealed class TotalOrderIeee754ComparerTests | ||
{ | ||
public static IEnumerable<object[]> SingleTestData | ||
{ | ||
get | ||
{ | ||
yield return new object[] { 0.0f, 0.0f, 0 }; | ||
yield return new object[] { -0.0f, -0.0f, 0 }; | ||
yield return new object[] { 0.0f, -0.0f, 1 }; | ||
yield return new object[] { -0.0f, 0.0f, -1 }; | ||
yield return new object[] { 0.0f, 1.0f, -1 }; | ||
yield return new object[] { float.PositiveInfinity, 1.0f, 1 }; | ||
yield return new object[] { BitConverter.UInt32BitsToSingle(0xFFC00000), float.NegativeInfinity, -1 }; | ||
yield return new object[] { BitConverter.UInt32BitsToSingle(0xFFC00000), -1.0f, -1 }; | ||
yield return new object[] { BitConverter.UInt32BitsToSingle(0x7FC00000), 1.0f, 1 }; | ||
yield return new object[] { BitConverter.UInt32BitsToSingle(0x7FC00000), float.PositiveInfinity, 1 }; | ||
yield return new object[] { float.NaN, float.NaN, 0 }; | ||
yield return new object[] { BitConverter.UInt32BitsToSingle(0xFFC00000), BitConverter.UInt32BitsToSingle(0x7FC00000), -1 }; | ||
yield return new object[] { BitConverter.UInt32BitsToSingle(0x7FC00000), BitConverter.UInt32BitsToSingle(0x7FC00001), -1 }; // implementation defined, not part of IEEE 754 totalOrder | ||
} | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(SingleTestData))] | ||
public void TotalOrderTestSingle(float x, float y, int result) | ||
{ | ||
var comparer = new TotalOrderIeee754Comparer<float>(); | ||
Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); | ||
} | ||
|
||
public static IEnumerable<object[]> DoubleTestData | ||
{ | ||
get | ||
{ | ||
yield return new object[] { 0.0, 0.0, 0 }; | ||
yield return new object[] { -0.0, -0.0, 0 }; | ||
yield return new object[] { 0.0, -0.0, 1 }; | ||
yield return new object[] { -0.0, 0.0, -1 }; | ||
yield return new object[] { 0.0, 1.0, -1 }; | ||
yield return new object[] { double.PositiveInfinity, 1.0, 1 }; | ||
yield return new object[] { BitConverter.UInt64BitsToDouble(0xFFF80000_00000000), double.NegativeInfinity, -1 }; | ||
yield return new object[] { BitConverter.UInt64BitsToDouble(0xFFF80000_00000000), -1.0, -1 }; | ||
yield return new object[] { BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), 1.0, 1 }; | ||
yield return new object[] { BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), double.PositiveInfinity, 1 }; | ||
yield return new object[] { double.NaN, double.NaN, 0 }; | ||
yield return new object[] { BitConverter.UInt64BitsToDouble(0xFFF80000_00000000), BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), -1 }; | ||
yield return new object[] { BitConverter.UInt64BitsToDouble(0x7FF80000_00000000), BitConverter.UInt64BitsToDouble(0x7FF80000_00000001), -1 }; // implementation defined, not part of IEEE 754 totalOrder | ||
} | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(DoubleTestData))] | ||
public void TotalOrderTestDouble(double x, double y, int result) | ||
{ | ||
var comparer = new TotalOrderIeee754Comparer<double>(); | ||
Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); | ||
} | ||
public static IEnumerable<object[]> HalfTestData | ||
{ | ||
get | ||
{ | ||
yield return new object[] { (Half)0.0, (Half)0.0, 0 }; | ||
yield return new object[] { (Half)(-0.0), (Half)(-0.0), 0 }; | ||
yield return new object[] { (Half)0.0, (Half)(-0.0), 1 }; | ||
yield return new object[] { (Half)(-0.0), (Half)0.0, -1 }; | ||
yield return new object[] { (Half)0.0, (Half)1.0, -1 }; | ||
yield return new object[] { Half.PositiveInfinity, (Half)1.0, 1 }; | ||
yield return new object[] { BitConverter.UInt16BitsToHalf(0xFE00), Half.NegativeInfinity, -1 }; | ||
yield return new object[] { BitConverter.UInt16BitsToHalf(0xFE00), (Half)(-1.0), -1 }; | ||
yield return new object[] { BitConverter.UInt16BitsToHalf(0x7E00), (Half)1.0, 1 }; | ||
yield return new object[] { BitConverter.UInt16BitsToHalf(0x7E00), Half.PositiveInfinity, 1 }; | ||
yield return new object[] { Half.NaN, Half.NaN, 0 }; | ||
yield return new object[] { BitConverter.UInt16BitsToHalf(0xFE00), BitConverter.UInt16BitsToHalf(0x7E00), -1 }; | ||
yield return new object[] { BitConverter.UInt16BitsToHalf(0x7E00), BitConverter.UInt16BitsToHalf(0x7E01), -1 }; // implementation defined, not part of IEEE 754 totalOrder | ||
} | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(HalfTestData))] | ||
public void TotalOrderTestHalf(Half x, Half y, int result) | ||
{ | ||
var comparer = new TotalOrderIeee754Comparer<Half>(); | ||
Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(SingleTestData))] | ||
public void TotalOrderTestNFloat(float x, float y, int result) | ||
{ | ||
var comparer = new TotalOrderIeee754Comparer<NFloat>(); | ||
Assert.Equal(result, Math.Sign(comparer.Compare(x, y))); | ||
} | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.