You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
System.ValueType has a default behavior that is done by the runtime where effectively calls Equals on each field. I expect it wasn't updated to consider InlineArray at all, potentially by design.
I think we should make Equals work. The CanCompareBits part could be efficient and field-wise compare fallback is hopelessly slow anyways.
By looking at the implementation it seems like the effort is to provide a behavior that recursively compares field's bits , which is a reasonably correct behavior, no matter how slow it is - likely the assumption is that incomplete/throwing implementation would be worse.
Also should check GetHashcode
The comment says "We look for the first non-static field and get its hashcode." I think that would be acceptable. Just need to be sure it indeed does that for inlined arrays.
It looks like the auto-generated equality for that struct does a bitwise-compare. Shouldn't we do the same for InlineArray?
I don't see an auto-generated equality. I see this falling back to the ValueType.Equals() implementation that will do, as you mention, a bit-wise check. However, if one changes the field to object, fields will be examined.
How are you viewing the relationship between the Size and InlineArray? One alters the size of the ValueType - not fields, while the other impliciltly introduces new, well-defined, fields.
It looks like the auto-generated equality for that struct does a bitwise-compare.
It does not. Run the following:
using System.Runtime.InteropServices;
unsafe
{
Buffer b1 = new Buffer();
*((int*)&b1 + 1) = 2;
Buffer b2 = new Buffer();
*((int*)&b2 + 1) = 3;
Console.WriteLine(b1.Equals(b2));
}
[StructLayout(LayoutKind.Sequential, Size = 40)]
public struct Buffer
{
public int Field;
}
It prints true. It compares the Field only. It does not compare the remaining 36 bytes. There is no obvious right way to compare the remaining 36 bytes. We do not know what's actually stored in there bytes.
If ValueType.Equals compares fields, and [InlineArray(N)] is a conceptual N fields in sequential order, then it seems wrong to only compare the first element (i.e., the sole field). It also seems wrong to throw when a proper, correct method exists: Span<T>.SequenceEquals. The default JIT-based implementation could just forward to that. Forcing users to write it themselves is the same logic as saying ValueType.Equals shouldn't exist: "just have the user override Object.Equals themselves!"
@colejohnson66 It is a fair opinion, but not an obviously "correct" one.
Forcing users to write it themselves is the same logic as saying ValueType.Equals shouldn't exist: "just have the user override Object.Equals themselves!"
Your last statement, above, is at least part of the reasoning. Providing a default for an equality value check has been contentious for a long time and one that we would likely not make again.
Aside from the opinions regarding default equality, there are other considerations too. Specifically, common uses of the InlineArrayAttribute feature, inherent complexity in what many consider the correct default implementation, and finally performance of the default implementation.
A common use case for InlineArrayAttribute is as a scratch or temporary buffer that may be partially set. This means that simply doing a naive bitwise check can be incorrect. It is reasonable to say the default of comparing the entire buffer is correct, but this leads into the second consideration, complexity. If the InlineArrayAttribute is a set of object instances, we can't perform a simple bitwise check, but instead must iterate over each of them, check for null, and then call Equals(). All this can be done with Span<object> so this isn't a technical issue, but rather one of complexity. Finally, let's consider the performance angle. The default implementation uses reflection and so is, by default, slow and not AOT friendly. None of this push back is technical, but rather a debate about what is correct, intuitive, and performant, and if it is worth investing in a default implementation. In the opinion of people on the runtime team, it isn't.
User defined ValueTypes that expect to have their Equals() or GetHashCode() called, should be overridden by the user.
I would like to remember, that according to docsGetHashCode should not throw exceptions.
The GetHashCode method should not throw exceptions.
Yes, that is the guidance here. The issue we face is what is the correct result of this and the intent of that statement. My reading, which may be wrong, is that is intented to provide guidance to users overridding the default, not necessarily a restriction on a default implementation provided by the runtime.
We can look to @stephentoub or @jkotas for clarification on this point. The concern I have would be the following expectation on the function:
A hash function is used to quickly generate a number (hash code) that corresponds to the value of an object.
The intended value of the object is unlikely to be simply the first field. This would imply it is clearer to fail (throw an exception) and inform the user the default "result" is suspect rather than let them consider the result useful.
But you can't use spans as dictionary key or box it unlike inline array and it does not violate object contract.
I think mentioning Dictionary<,> is only reinforcing the opinion that users should be overriding these methods and properly defining them so they are what is expected.
Activity
tannergooding commentedon Jan 13, 2024
CC. @VSadov, @jkotas
This is likely also a consideration for
GetHashCode. The workaround here is to useSequenceEqualover the span that C# will let you implicitly create.VSadov commentedon Jan 23, 2024
Where does
Default Equals implementationcome from? The type system only provides the size and layout.I think ideally there should either be no default implementation or it should consider all elements.
tannergooding commentedon Jan 23, 2024
System.ValueTypehas a default behavior that is done by the runtime where effectively calls Equals on each field. I expect it wasn't updated to considerInlineArrayat all, potentially by design.jkotas commentedon Jan 23, 2024
System.ValueType.Equalsfor CoreCLR is here:runtime/src/coreclr/System.Private.CoreLib/src/System/ValueType.cs
Lines 25 to 67 in d44bc24
Our docs recommend overriding it for correctness and performance reason: https://learn.microsoft.com/en-us/dotnet/api/system.valuetype.equals
Should we throw from the default implementation for InlineArrays? (It would be a breaking change.)
VSadov commentedon Jan 23, 2024
I think we should make Equals work. The
CanCompareBitspart could be efficient and field-wise compare fallback is hopelessly slow anyways.By looking at the implementation it seems like the effort is to provide a behavior that recursively compares field's bits , which is a reasonably correct behavior, no matter how slow it is - likely the assumption is that incomplete/throwing implementation would be worse.
Also should check
GetHashcodeThe comment says "We look for the first non-static field and get its hashcode." I think that would be acceptable. Just need to be sure it indeed does that for inlined arrays.
AaronRobinsonMSFT commentedon Jun 14, 2024
Yes, I think we should.
agocke commentedon Jun 17, 2024
Why would the
InlineArraybehavior be different than structs with manual size? That is, if you writeIt looks like the auto-generated equality for that struct does a bitwise-compare. Shouldn't we do the same for InlineArray?
AaronRobinsonMSFT commentedon Jun 17, 2024
I don't see an auto-generated equality. I see this falling back to the
ValueType.Equals()implementation that will do, as you mention, a bit-wise check. However, if one changes the field toobject, fields will be examined.How are you viewing the relationship between the
SizeandInlineArray? One alters the size of the ValueType - not fields, while the other impliciltly introduces new, well-defined, fields.jkotas commentedon Jun 17, 2024
It does not. Run the following:
It prints true. It compares the
Fieldonly. It does not compare the remaining 36 bytes. There is no obvious right way to compare the remaining 36 bytes. We do not know what's actually stored in there bytes.agocke commentedon Jun 17, 2024
Oops. Yes, I had a bug in my test program.
colejohnson66 commentedon Jun 18, 2024
If
ValueType.Equalscompares fields, and[InlineArray(N)]is a conceptualNfields in sequential order, then it seems wrong to only compare the first element (i.e., the sole field). It also seems wrong to throw when a proper, correct method exists:Span<T>.SequenceEquals. The default JIT-based implementation could just forward to that. Forcing users to write it themselves is the same logic as sayingValueType.Equalsshouldn't exist: "just have the user overrideObject.Equalsthemselves!"AaronRobinsonMSFT commentedon Jun 18, 2024
@colejohnson66 It is a fair opinion, but not an obviously "correct" one.
Your last statement, above, is at least part of the reasoning. Providing a default for an equality value check has been contentious for a long time and one that we would likely not make again.
Aside from the opinions regarding default equality, there are other considerations too. Specifically, common uses of the
InlineArrayAttributefeature, inherent complexity in what many consider the correct default implementation, and finally performance of the default implementation.A common use case for
InlineArrayAttributeis as a scratch or temporary buffer that may be partially set. This means that simply doing a naive bitwise check can be incorrect. It is reasonable to say the default of comparing the entire buffer is correct, but this leads into the second consideration, complexity. If theInlineArrayAttributeis a set ofobjectinstances, we can't perform a simple bitwise check, but instead must iterate over each of them, check fornull, and then callEquals(). All this can be done withSpan<object>so this isn't a technical issue, but rather one of complexity. Finally, let's consider the performance angle. The default implementation uses reflection and so is, by default, slow and not AOT friendly. None of this push back is technical, but rather a debate about what is correct, intuitive, and performant, and if it is worth investing in a default implementation. In the opinion of people on the runtime team, it isn't.User defined
ValueTypes that expect to have theirEquals()orGetHashCode()called, should be overridden by the user.Ilchert commentedon Jun 19, 2024
I would like to remember, that according to docs
GetHashCodeshould not throw exceptions.AaronRobinsonMSFT commentedon Jun 19, 2024
Yes, that is the guidance here. The issue we face is what is the correct result of this and the intent of that statement. My reading, which may be wrong, is that is intented to provide guidance to users overridding the default, not necessarily a restriction on a default implementation provided by the runtime.
We can look to @stephentoub or @jkotas for clarification on this point. The concern I have would be the following expectation on the function:
The intended value of the object is unlikely to be simply the first field. This would imply it is clearer to fail (throw an exception) and inform the user the default "result" is suspect rather than let them consider the result useful.
jkotas commentedon Jun 19, 2024
This is not the first GetHashCode/Equals that throws NotSupportedException. Span/ReadOnlySpan is an example of prior art: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/ReadOnlySpan.cs#L181-L201.
Right, the poor hashcode distribution of the default implementation is one of the common problems that people run into.
Ilchert commentedon Jun 19, 2024
But you can't use spans as dictionary key or box it unlike inline array and it does not violate
objectcontract.AaronRobinsonMSFT commentedon Jun 20, 2024
I think mentioning
Dictionary<,>is only reinforcing the opinion that users should be overriding these methods and properly defining them so they are what is expected.Ilchert commentedon Jun 20, 2024
Looks like we should be extremely careful when using
InlineArrayand read the manual before using :)AaronRobinsonMSFT commentedon Jun 20, 2024
Yep. Here is the updated doc PR if you'd like to see how we are documenting this - dotnet/dotnet-api-docs#10036