Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8b612a6
Generalize IsBitwiseEquatable to field-wise IEquatable value types
tannergooding Jul 14, 2026
f2fd52a
Fix IsNotTightlyPacked for multi-byte value-type fields
tannergooding Jul 14, 2026
5334651
Recognize recursive value-type fields in IsBitwiseEquatable
tannergooding Jul 14, 2026
5efb9f5
Recognize record struct EqualityComparer<T>.Default.Equals in IsBitwi…
tannergooding Jul 14, 2026
ec0e64e
Keep inline array types uncached in CanCompareBitsOrUseFastGetHashCode
tannergooding Jul 14, 2026
dbc47aa
Address review feedback on token decode and comments
tannergooding Jul 14, 2026
4b35aa7
Guard the ILC field-wise scan against a truncated IL stream
tannergooding Jul 14, 2026
06fbe39
Add CoreCLR test coverage for IsBitwiseEquatable
tannergooding Jul 15, 2026
2c79ee3
Force optimized IL and correct Guid case in the CoreCLR test
tannergooding Jul 15, 2026
0f7aa7e
Gate the CoreCLR IsBitwiseEquatable test off Mono
tannergooding Jul 15, 2026
5a57cd2
Treat enum fields as bitwise-comparable in the VM Equals scanner
tannergooding Jul 15, 2026
16b9789
Guard the ILC field-wise scan against malformed IL
tannergooding Jul 15, 2026
aeeebda
Use MemberData so the test builds in the merged runner
tannergooding Jul 15, 2026
165ff66
Exercise IsBitwiseEquatable via UnsafeAccessor and drop the ILC unit …
tannergooding Jul 15, 2026
eee0912
Recognize long-form branches and unify the IsBitwiseEquatable decision
tannergooding Jul 16, 2026
0826af2
Make Guid.Equals a field-wise comparison
tannergooding Jul 16, 2026
8dc6c24
Funnel bitwise-equatable struct equality through Equals
tannergooding Jul 16, 2026
3e13ed0
Cover long-form branches in the BitwiseEquatable test
tannergooding Jul 16, 2026
d4be11d
Rethrow terminal exceptions from the IsBitwiseEquatable scan
tannergooding Jul 16, 2026
9b36292
Document the tearing consideration on IsBitwiseEquatable
tannergooding Jul 16, 2026
2fb72c9
Apply suggestions from code review
tannergooding Jul 16, 2026
627879f
Potential fix for pull request finding
tannergooding Jul 16, 2026
82db303
Bail out early on generic value types in the field-wise scan
tannergooding Jul 16, 2026
89c6fc4
Guard the field-wise scan against writing past the field array
tannergooding Jul 16, 2026
ac384b3
Address review feedback on the field-wise equality scan
tannergooding Jul 20, 2026
1df9e78
Support generic value types in the field-wise equality scan
tannergooding Jul 20, 2026
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 @@ -352,6 +352,13 @@ public static object GetUninitializedObject(
/// <returns>true if given type is bitwise equatable (memcmp can be used for equality checking)</returns>
/// <remarks>
/// Only use the result of this for Equals() comparison, not for CompareTo() comparison.
/// <para>
/// A bitwise comparison may read the value using accesses wider than an individual field. Under an
/// unsynchronized concurrent mutation -- already a data race with undefined behavior -- this can observe
/// a torn value within a single field that a strictly field-wise comparison would not. A torn read cannot
/// fabricate an invalid managed reference; only the already-undefined total comparison result is affected. This is
/// acceptable for bitwise-based APIs such as <see cref="MemoryExtensions.SequenceEqual{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>.
/// </para>
/// </remarks>
[Intrinsic]
internal static bool IsBitwiseEquatable<T>()
Expand Down
10 changes: 10 additions & 0 deletions src/coreclr/nativeaot/Test.CoreLib/src/System/IEquatable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System
{
public interface IEquatable<T>
{
bool Equals(T other);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
<Compile Include="System\SpanHelpers.cs" />
<Compile Include="System\RuntimeExceptionHelpers.cs" />
<Compile Include="System\Object.cs" />
<Compile Include="System\IEquatable.cs" />
<Compile Include="System\Type.cs" />
<Compile Include="System\RuntimeTypeHandle.cs" />
<Compile Include="$(AotCommonPath)\Internal\Runtime\TypeManagerHandle.cs">
Expand Down
388 changes: 388 additions & 0 deletions src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;

using Internal.Text;
using Internal.TypeSystem;

using Debug = System.Diagnostics.Debug;
Expand Down Expand Up @@ -32,51 +31,8 @@ public static MethodIL EmitIL(MethodDesc method)
bool result;
if (method.Name == "IsBitwiseEquatable"u8)
{
// Ideally we could detect automatically whether a type is trivially equatable
// (i.e., its operator == could be implemented via memcmp). But for now we'll
// do the simple thing and hardcode the list of types we know fulfill this contract.
// n.b. This doesn't imply that the type's CompareTo method can be memcmp-implemented,
// as a method like CompareTo may need to take a type's signedness into account.
switch (elementType.UnderlyingType.Category)
{
case TypeFlags.Boolean:
case TypeFlags.Byte:
case TypeFlags.SByte:
case TypeFlags.Char:
case TypeFlags.UInt16:
case TypeFlags.Int16:
case TypeFlags.UInt32:
case TypeFlags.Int32:
case TypeFlags.UInt64:
case TypeFlags.Int64:
case TypeFlags.IntPtr:
case TypeFlags.UIntPtr:
result = true;
break;
default:
result = false;
if (elementType is MetadataType mdType)
{
if (IsKnownBitwiseEquatableType(mdType))
{
result = true;
}
else if (mdType.IsValueType)
{
bool? equatable = ComparerIntrinsics.ImplementsIEquatable(mdType.GetTypeDefinition());

if (equatable.HasValue && !equatable.Value)
{
// Value type that can use memcmp and that doesn't override object.Equals or implement IEquatable<T>.Equals.
MethodDesc objectEquals = mdType.Context.GetWellKnownType(WellKnownType.Object).GetMethod("Equals"u8, null);
result =
mdType.FindVirtualFunctionTargetMethodOnObjectType(objectEquals).OwningType != mdType &&
ComparerIntrinsics.CanCompareValueTypeBits(mdType, objectEquals);
}
}
}
break;
}
// The runtime and the ILC share a single determination of what is bitwise-equatable.
result = ComparerIntrinsics.IsBitwiseEquatable(elementType);
}
else
{
Expand All @@ -87,21 +43,5 @@ public static MethodIL EmitIL(MethodDesc method)

return new ILStubMethodIL(method, new byte[] { (byte)opcode, (byte)ILOpcode.ret }, Array.Empty<LocalVariableDefinition>(), Array.Empty<object>());
}

private static bool IsKnownBitwiseEquatableType(MetadataType type)
{
if (type.Module != type.Context.SystemModule)
{
return false;
}

Utf8Span ns = type.Namespace;
if (ns == "System"u8)
{
Utf8Span name = type.Name;
return name == "Guid"u8 || name == "Int128"u8 || name == "UInt128"u8;
}
return ns == "System.Text"u8 && type.Name == "Rune"u8;
}
}
}
12 changes: 10 additions & 2 deletions src/coreclr/vm/comutilnative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1775,9 +1775,17 @@ BOOL CanCompareBitsOrUseFastGetHashCode(MethodTable* mt)
return mt->CanCompareBitsOrUseFastGetHashCode();
}

if (mt->GetClass()->IsInlineArray())
{
// Inline arrays must always throw from ValueType.Equals/GetHashCode, which only happens on the
// QCALL entry point. Return false without caching so the managed fast path keeps routing there
// instead of reading a cached 'false' (e.g. primed by an enclosing type's field recursion) that
// would silently skip the throw.
return FALSE;
}

if (mt->ContainsGCPointers()
|| mt->IsNotTightlyPacked()
|| mt->GetClass()->IsInlineArray())
|| !mt->IsTightlyPacked())
{
mt->SetHasCheckedCanCompareBitsOrUseFastGetHashCode();
return FALSE;
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,6 @@ DEFINE_FIELD(ENC_HELPER, OBJECT_REFERENCE, _objectReference)

Comment thread
tannergooding marked this conversation as resolved.
DEFINE_CLASS(ENCODING, Text, Encoding)

DEFINE_CLASS(RUNE, Text, Rune)

DEFINE_CLASS(ENUM, System, Enum)

DEFINE_CLASS(ENVIRONMENT, System, Environment)
Expand Down Expand Up @@ -1301,6 +1299,7 @@ DEFINE_CLASS(ICOMPARABLEGENERIC, System, IComparable`1)
DEFINE_METHOD(ICOMPARABLEGENERIC, COMPARE_TO, CompareTo, NoSig)

DEFINE_CLASS(IEQUATABLEGENERIC, System, IEquatable`1)
DEFINE_METHOD(IEQUATABLEGENERIC, EQUALS, Equals, NoSig)

DEFINE_CLASS_U(Reflection, LoaderAllocator, LoaderAllocatorObject)
DEFINE_FIELD_U(m_slots, LoaderAllocatorObject, m_pSlots)
Expand Down Expand Up @@ -1330,6 +1329,7 @@ DEFINE_METHOD(UTF8BUFFERMARSHALER, CONVERT_TO_MANAGED, ConvertToManaged, NoSig)

// Classes referenced in EqualityComparer<T>.Default optimization

DEFINE_CLASS(EQUALITY_COMPARER, CollectionsGeneric, EqualityComparer`1)
DEFINE_CLASS(STRING_EQUALITYCOMPARER, CollectionsGeneric, StringEqualityComparer)
DEFINE_CLASS(ENUM_EQUALITYCOMPARER, CollectionsGeneric, EnumEqualityComparer`1)
DEFINE_CLASS(NULLABLE_EQUALITYCOMPARER, CollectionsGeneric, NullableEqualityComparer`1)
Expand Down
Loading
Loading