diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs
index 584f6cd3e15965..f8460dd4c2e8f7 100644
--- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs
+++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs
@@ -352,6 +352,13 @@ public static object GetUninitializedObject(
/// true if given type is bitwise equatable (memcmp can be used for equality checking)
///
/// Only use the result of this for Equals() comparison, not for CompareTo() comparison.
+ ///
+ /// 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 .
+ ///
///
[Intrinsic]
internal static bool IsBitwiseEquatable()
diff --git a/src/coreclr/nativeaot/Test.CoreLib/src/System/IEquatable.cs b/src/coreclr/nativeaot/Test.CoreLib/src/System/IEquatable.cs
new file mode 100644
index 00000000000000..d694629537297c
--- /dev/null
+++ b/src/coreclr/nativeaot/Test.CoreLib/src/System/IEquatable.cs
@@ -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
+ {
+ bool Equals(T other);
+ }
+}
diff --git a/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj b/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj
index e1b9c28a56c2c9..f33002cf626c01 100644
--- a/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj
+++ b/src/coreclr/nativeaot/Test.CoreLib/src/Test.CoreLib.csproj
@@ -236,6 +236,7 @@
+
diff --git a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs
index e183c0f85b9d44..7760d7d1f39fb2 100644
--- a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs
+++ b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs
@@ -6,6 +6,7 @@
using System.Collections.Generic;
using Internal.TypeSystem;
+using Internal.TypeSystem.Ecma;
using Debug = System.Diagnostics.Debug;
@@ -317,6 +318,393 @@ public static bool CanCompareValueTypeBitsUntilOffset(MetadataType type, MethodD
return result;
}
+ ///
+ /// Determines whether is bitwise-equatable: an unmanaged, tightly-packed
+ /// type whose equality is provably a bitwise (memcmp) comparison. This is the single authority
+ /// behind .
+ ///
+ public static bool IsBitwiseEquatable(TypeDesc type)
+ {
+ // Integer-like primitives, enums, native ints, and pointers are memcmp-comparable.
+ if (IsBitwiseComparablePrimitive(type))
+ return true;
+
+ if (type is not MetadataType mdType || !mdType.IsValueType)
+ return false;
+
+ bool? equatable = ImplementsIEquatable(mdType);
+ if (!equatable.HasValue)
+ return false;
+
+ if (equatable.Value)
+ {
+ // Value type that implements IEquatable of self: bitwise-equatable when it is tightly
+ // packed and its Equals is a plain field-wise comparison.
+ return IsIEquatableEqualsFieldwise(mdType);
+ }
+
+ // Value type that can use memcmp and that doesn't override object.Equals or implement
+ // IEquatable.Equals.
+ MethodDesc objectEquals = mdType.Context.GetWellKnownType(WellKnownType.Object).GetMethod("Equals"u8, null);
+ return mdType.FindVirtualFunctionTargetMethodOnObjectType(objectEquals).OwningType != mdType
+ && CanCompareValueTypeBits(mdType, objectEquals);
+ }
+
+ ///
+ /// Determines whether a value type's implementation of self is a
+ /// plain field-wise comparison that is equivalent to a bitwise (memcmp) comparison. This lets a type
+ /// that implements IEquatable<T> still be reported as bitwise-equatable when its Equals does
+ /// nothing more than compare every field with ==.
+ ///
+ public static bool IsIEquatableEqualsFieldwise(MetadataType type)
+ {
+ // Unmanaged (so a byte-wise compare is meaningful) and tightly packed (no padding anywhere the
+ // compare would inspect) -- matching the CoreCLR VM, which checks these separately.
+ if (type.ContainsGCPointers)
+ return false;
+
+ if (!IsTightlyPacked(type))
+ return false;
+
+ MethodDesc equalsImpl = GetIEquatableEqualsImplementation(type);
+ if (equalsImpl == null)
+ return false;
+
+ MethodIL methodIL = GetScannableMethodIL(equalsImpl);
+ if (methodIL == null)
+ return false;
+
+ // A common pattern forwards `bool Equals(T other) => this == other;` to a user-defined
+ // `op_Equality`. Follow that single forward before scanning the field-wise comparison.
+ if (TryGetOpEqualityForward(methodIL, type) is MethodDesc forwarded)
+ {
+ methodIL = GetScannableMethodIL(forwarded);
+ if (methodIL == null)
+ return false;
+ }
+
+ return ScanFieldwiseEqualsBody(methodIL, type);
+ }
+
+ // Builds the IL to scan for a method that may live on an instantiated type. The IL is defined on the
+ // typical (open) method; wrapping it in an InstantiatedMethodIL makes token lookups resolve fields
+ // and methods in the exact instantiation. Returns null if the method has no ECMA-backed body.
+ private static MethodIL GetScannableMethodIL(MethodDesc method)
+ {
+ if (method.GetTypicalMethodDefinition() is not EcmaMethod typicalMethod)
+ return null;
+
+ MethodIL typicalIL = EcmaMethodIL.Create(typicalMethod);
+ if (typicalIL == null)
+ return null;
+
+ return method == typicalMethod ? typicalIL : new InstantiatedMethodIL(method, typicalIL);
+ }
+
+ private static bool IsTightlyPacked(MetadataType type)
+ {
+ // Mirrors the CoreCLR VM's MethodTable::IsTightlyPacked: a byte-wise compare equals comparing
+ // every field only if there is no padding anywhere. That needs the declared fields to exactly
+ // cover the instance size (no gaps, no overlap) and every nested value-type field to itself be
+ // tightly packed. The nested check makes this transitive.
+ if (type.IsInlineArray)
+ return false;
+
+ if (type.IsGenericDefinition)
+ return false;
+
+ OverlappingFieldTracker overlappingFieldTracker = new OverlappingFieldTracker(type);
+ int lastFieldEndOffset = 0;
+
+ foreach (FieldDesc field in type.GetFields())
+ {
+ if (field.IsStatic)
+ continue;
+
+ lastFieldEndOffset = Math.Max(lastFieldEndOffset, field.Offset.AsInt + field.FieldType.GetElementSize().AsInt);
+
+ if (!overlappingFieldTracker.TrackField(field))
+ return false;
+
+ TypeDesc fieldType = field.FieldType;
+ if (fieldType.IsValueType && !fieldType.IsPrimitive && !fieldType.IsEnum)
+ {
+ // Nested value type: recurse for transitive packing. Primitives, pointers, and
+ // references are leaves whose element size already accounts for their footprint.
+ if (fieldType is not MetadataType nestedType || !IsTightlyPacked(nestedType))
+ return false;
+ }
+ }
+
+ if (overlappingFieldTracker.HasGapsBeforeOffset(lastFieldEndOffset))
+ return false;
+
+ return lastFieldEndOffset == type.InstanceFieldSize.AsInt;
+ }
+
+ private static MethodDesc GetIEquatableEqualsImplementation(MetadataType type)
+ {
+ MetadataType iequatableType = type.Context.SystemModule.GetKnownType("System"u8, "IEquatable`1"u8);
+ MethodDesc equalsInterfaceMethod = iequatableType.MakeInstantiatedType(type).GetMethod("Equals"u8, null);
+ if (equalsInterfaceMethod == null)
+ return null;
+
+ return type.ResolveInterfaceMethodToVirtualMethodOnType(equalsInterfaceMethod);
+ }
+
+ private static MethodDesc TryGetOpEqualityForward(MethodIL methodIL, MetadataType type)
+ {
+ // ldarg.0; ldobj T; ldarg.1; call op_Equality; ret
+ ILReader reader = new ILReader(methodIL.GetILBytes());
+
+ if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldarg_0)
+ return null;
+ if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldobj)
+ return null;
+ if (methodIL.GetObject(reader.ReadILToken()) as TypeDesc != type)
+ return null;
+ if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldarg_1)
+ return null;
+ if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.call)
+ return null;
+ MethodDesc callee = methodIL.GetObject(reader.ReadILToken()) as MethodDesc;
+ if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ret)
+ return null;
+ if (reader.HasNext)
+ return null;
+
+ if (callee == null || !callee.Signature.IsStatic || callee.OwningType != type || callee.Name != "op_Equality"u8)
+ return null;
+
+ return callee;
+ }
+
+ private static bool ScanFieldwiseEqualsBody(MethodIL methodIL, MetadataType type)
+ {
+ try
+ {
+ return ScanFieldwiseEqualsBodyCore(methodIL, type);
+ }
+ catch (TypeSystemException.InvalidProgramException)
+ {
+ // Malformed or truncated IL: stay conservative and treat it as not field-wise.
+ return false;
+ }
+ }
+
+ private static bool ScanFieldwiseEqualsBodyCore(MethodIL methodIL, MetadataType type)
+ {
+ // Verifies the body is a plain field-wise equality: every instance field is compared exactly once
+ // (via `==`, its own `Equals`, or `EqualityComparer.Default.Equals`) and the results are ANDed
+ // together, which is equivalent to a bitwise (memcmp) comparison.
+ int instanceFieldCount = 0;
+ foreach (FieldDesc field in type.GetFields())
+ {
+ if (!field.IsStatic)
+ instanceFieldCount++;
+ }
+
+ if (instanceFieldCount == 0)
+ return false;
+
+ HashSet comparedFields = new HashSet();
+ ILReader reader = new ILReader(methodIL.GetILBytes());
+
+ int falseTarget = -1;
+ bool sawFinalCompare = false;
+
+ while (!sawFinalCompare)
+ {
+ if (!reader.HasNext)
+ return false;
+
+ // Optional EqualityComparer.Default lead-in: `call EqualityComparer::get_Default`
+ // before the operands.
+ MethodDesc getDefault = null;
+ if (reader.PeekILOpcode() == ILOpcode.call)
+ {
+ reader.ReadILOpcode();
+ getDefault = methodIL.GetObject(reader.ReadILToken()) as MethodDesc;
+ }
+
+ // Left operand: `ldarg.0; ldfld/ldflda F`. The EqualityComparer and inline `==` forms load by
+ // value; the `.Equals` call form loads the left side by address.
+ if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldarg_0)
+ return false;
+
+ ILOpcode leftLoad = reader.ReadILOpcode();
+ if (leftLoad != ILOpcode.ldfld && leftLoad != ILOpcode.ldflda)
+ return false;
+ if (getDefault != null && leftLoad != ILOpcode.ldfld)
+ return false;
+ FieldDesc leftField = methodIL.GetObject(reader.ReadILToken()) as FieldDesc;
+
+ if (reader.ReadILOpcode() != ILOpcode.ldarg_1)
+ return false;
+ if (reader.ReadILOpcode() != ILOpcode.ldfld)
+ return false;
+ FieldDesc rightField = methodIL.GetObject(reader.ReadILToken()) as FieldDesc;
+
+ if (leftField == null || leftField != rightField || leftField.IsStatic || leftField.OwningType != type)
+ return false;
+
+ // Each field must be compared exactly once.
+ if (!comparedFields.Add(leftField))
+ return false;
+
+ if (getDefault == null && leftLoad == ILOpcode.ldfld)
+ {
+ // Inline `==`: only integer-like primitives are memcmp-equivalent.
+ if (!IsBitwiseComparablePrimitive(leftField.FieldType))
+ return false;
+
+ ILOpcode compareOpcode = reader.ReadILOpcode();
+ if (compareOpcode == ILOpcode.bne_un_s || compareOpcode == ILOpcode.bne_un)
+ {
+ // Non-final field: `bne.un[.s] FALSE` jumps to the shared `return false` tail. A
+ // body larger than a signed-byte range uses the long form.
+ int target = reader.ReadBranchDestination(compareOpcode);
+ if (falseTarget == -1)
+ falseTarget = target;
+ else if (falseTarget != target)
+ return false;
+ }
+ else if (compareOpcode == ILOpcode.ceq)
+ {
+ // Final field: `ceq; ret` produces the result directly.
+ if (reader.ReadILOpcode() != ILOpcode.ret)
+ return false;
+ sawFinalCompare = true;
+ }
+ else
+ {
+ return false;
+ }
+
+ continue;
+ }
+
+ if (getDefault != null)
+ {
+ // `callvirt EqualityComparer::Equals(!0, !0)`.
+ if (reader.ReadILOpcode() != ILOpcode.callvirt)
+ return false;
+ MethodDesc equals = methodIL.GetObject(reader.ReadILToken()) as MethodDesc;
+ if (!IsEqualityComparerDefaultEquals(getDefault, equals, leftField.FieldType))
+ return false;
+ }
+ else
+ {
+ // `.Equals` call form: a primitive's own Equals, or a nested type's field-wise Equals.
+ if (reader.ReadILOpcode() != ILOpcode.call)
+ return false;
+ MethodDesc callee = methodIL.GetObject(reader.ReadILToken()) as MethodDesc;
+ if (!IsPrimitiveEqualsCall(callee, leftField.FieldType) && !IsNestedFieldwiseEquatable(callee, leftField.FieldType))
+ return false;
+ }
+
+ // The Equals call already yields a bool: `brfalse[.s]` to the shared tail, or `ret` if final.
+ ILOpcode terminator = reader.ReadILOpcode();
+ if (terminator == ILOpcode.brfalse_s || terminator == ILOpcode.brfalse)
+ {
+ int target = reader.ReadBranchDestination(terminator);
+ if (falseTarget == -1)
+ falseTarget = target;
+ else if (falseTarget != target)
+ return false;
+ }
+ else if (terminator == ILOpcode.ret)
+ {
+ sawFinalCompare = true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ if (falseTarget != -1)
+ {
+ // Shared tail for a mismatch: `ldc.i4.0; ret`.
+ if (reader.Offset != falseTarget)
+ return false;
+ if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ldc_i4_0)
+ return false;
+ if (reader.ReadILOpcode() != ILOpcode.ret)
+ return false;
+ }
+
+ return !reader.HasNext && comparedFields.Count == instanceFieldCount;
+ }
+
+ private static bool IsNestedFieldwiseEquatable(MethodDesc callee, TypeDesc fieldType)
+ {
+ // The nested field must be compared through the nested type's own IEquatable.Equals, and
+ // that Equals must itself be field-wise (its layout is validated by IsIEquatableEqualsFieldwise).
+ if (callee == null || fieldType is not MetadataType nestedType || !nestedType.IsValueType)
+ return false;
+
+ if (callee != GetIEquatableEqualsImplementation(nestedType))
+ return false;
+
+ return IsIEquatableEqualsFieldwise(nestedType);
+ }
+
+ private static bool IsPrimitiveEqualsCall(MethodDesc callee, TypeDesc fieldType)
+ {
+ // A primitive field compared via 'x.Equals(y)' instead of 'x == y'; for these integer-like
+ // types both lower to the same bit-for-bit compare. Confirm the callee is its IEquatable.Equals.
+ if (callee == null || !IsBitwiseComparablePrimitive(fieldType))
+ return false;
+
+ return fieldType is MetadataType primitiveType
+ && callee == GetIEquatableEqualsImplementation(primitiveType);
+ }
+
+ private static bool IsEqualityComparerDefaultEquals(MethodDesc getDefault, MethodDesc equals, TypeDesc fieldType)
+ {
+ // A field compared with EqualityComparer.Default.Equals(this.F, other.F). That is a memcmp
+ // only when F is itself bitwise-equatable: a bit-comparable primitive, or a nested value type
+ // whose own IEquatable.Equals is field-wise.
+ if (!IsEqualityComparerMethod(getDefault, fieldType, "get_Default"u8, isStatic: true) ||
+ !IsEqualityComparerMethod(equals, fieldType, "Equals"u8, isStatic: false))
+ {
+ return false;
+ }
+
+ if (IsBitwiseComparablePrimitive(fieldType))
+ return true;
+
+ return fieldType is MetadataType nestedType && nestedType.IsValueType
+ && GetIEquatableEqualsImplementation(nestedType) != null
+ && IsIEquatableEqualsFieldwise(nestedType);
+ }
+
+ private static bool IsEqualityComparerMethod(MethodDesc method, TypeDesc fieldType, ReadOnlySpan name, bool isStatic)
+ {
+ if (method == null || method.Signature.IsStatic != isStatic || method.Name != name)
+ return false;
+
+ TypeDesc owningType = method.OwningType;
+ return owningType.GetTypeDefinition() is MetadataType definition
+ && definition.Module == fieldType.Context.SystemModule
+ && definition.Name == "EqualityComparer`1"u8
+ && definition.Namespace == "System.Collections.Generic"u8
+ && owningType.Instantiation.Length == 1
+ && owningType.Instantiation[0] == fieldType;
+ }
+
+ private static bool IsBitwiseComparablePrimitive(TypeDesc fieldType)
+ {
+ if (fieldType.IsPrimitive || fieldType.IsEnum || fieldType.IsPointer || fieldType.IsFunctionPointer)
+ {
+ TypeFlags category = fieldType.UnderlyingType.Category;
+ return category != TypeFlags.Single && category != TypeFlags.Double;
+ }
+
+ return false;
+ }
+
private struct OverlappingFieldTracker
{
private BitArray _usedBytes;
diff --git a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs
index 7ec96db0eb5fbb..473a4d9f3f15aa 100644
--- a/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs
+++ b/src/coreclr/tools/Common/TypeSystem/IL/Stubs/RuntimeHelpersIntrinsics.cs
@@ -3,7 +3,6 @@
using System;
-using Internal.Text;
using Internal.TypeSystem;
using Debug = System.Diagnostics.Debug;
@@ -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.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
{
@@ -87,21 +43,5 @@ public static MethodIL EmitIL(MethodDesc method)
return new ILStubMethodIL(method, new byte[] { (byte)opcode, (byte)ILOpcode.ret }, Array.Empty(), Array.Empty