Description
Background and motivation
Most of our vectorized implementations on MemoryExtensions check RuntimeHelpers.IsBitwiseEquatable to determine whether vectorization is feasible. Today, IsBitwiseEquatable is hardcoded to a finite list of primitive types; #75640 extends that further, but overriding Equals
/ implementing IEquatable<T>
opts you out again. Developers can still get the vectorization benefits of these implementations, but it's awkward and unsafe, e.g. with a type like:
struct MyColor : IEquatable<T> { ... }
instead of:
ReadOnlySpan<MyColor> colors = ...;
MyColor value = ...;
return colors.IndexOf(value);
it's something more like:
ReadOnlySpan<MyColor> colors = ...;
MyColor value = ...;
return MemoryMarshal.Cast<MyColor, int>(colors).IndexOf(Unsafe.As<MyColor, int>(ref value));
and most importantly, it's something someone has to write code for on each use rather than it "just work"ing.
If we instead expose a way for a type to be annotated as "You can trust that my Equals override and/or IEquatable implementation are identical to bitwise equality semantics", then we can special-case IsBitwiseEquatable to recognize this annotation, and everything else just lights up.
API Proposal
namespace System;
public interface IBitwiseEquatable<T> : IEquatable<T>
{
// no additional methods, it's just a marker interface
}
API Usage
struct MyColor : IBitwiseEquatable<MyColor>
{
private byte R, G, B, A;
public bool Equals(MyColor other) => R == other.R && G == other.G && B == other.B && A == other.A;
public override bool Equals(object obj) => obj is MyColor c && Equals(c);
public override int GetHashCode() => HashCode.Combine(R, G, B, A);
}
Alternative Designs
It could also be an attribute, e.g.
namespace System;
[AttributeUsage(AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
public sealed class BitwiseEquatableAttribute : Attribute
{
public BitwiseEquatableAttribute() { }
}
that could then be applied to the type, e.g.
[BitwiseEquatable]
struct MyColor : IEquatable<MyColor>
{
private byte R, G, B, A;
public bool Equals(MyColor other) => R == other.R && G == other.G && B == other.B && A == other.A;
public override bool Equals(object obj) => obj is MyColor c && Equals(c);
public override int GetHashCode() => HashCode.Combine(R, G, B, A);
}
Risks
It's yet one more thing a developer implementing equality would need to think about. We'd probably want the C# compiler to emit it in some situations for records, and anything we do around #48733 should also factor it in. We might also want analyzers that try to flag types which implement IBitwiseEquality but don't integrate all fields into its Equals, don't do a simple field-by-field comparison or unsafe-cast-memcmp in their Equals, etc.