Skip to content

Commit 5cb2a05

Browse files
Use fast hashing helpers for types with trailing padding (#118986)
This is same as #111620 but with an extra thing: enable unconditional method body folding on `__GetFieldHelper` overrides (both the simplified ones that just return a negative number and normal ones). Before this PR we considered valuetypes as byte-hashable/comparable only if the valuetype didn't have any trailing padding. This fixes things up so that we can consider types with trailing padding byte-hashable/comparable if the other requirements still hold (such as no double/float fields, no GC references, etc.) and there is a trailing padding. In this case, we remember the number of bytes that we should be looking at and ignore the rest. Since this doesn't seem to provide meaningful size savings according to rt-sz, this is a throughput improvement, not a size improvement.
1 parent 7157d6d commit 5cb2a05

File tree

4 files changed

+61
-23
lines changed

4 files changed

+61
-23
lines changed

src/coreclr/nativeaot/System.Private.CoreLib/src/System/ValueType.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,30 @@ public abstract class ValueType
3131
return this.GetType().ToString();
3232
}
3333

34-
private const int UseFastHelper = -1;
3534
private const int GetNumFields = -1;
3635

3736
// An override of this method will be injected by the compiler into all valuetypes that cannot be compared
38-
// using a simple memory comparison.
37+
// using a simple memory comparison until the last byte as reported by sizeof.
3938
// This API is a bit awkward because we want to avoid burning more than one vtable slot on this.
39+
// The method returns the offset and type handle of the index-th field on this type.
4040
// When index == GetNumFields, this method is expected to return the number of fields of this
41-
// valuetype. Otherwise, it returns the offset and type handle of the index-th field on this type.
41+
// valuetype or a negative value. If the value is negative, the struct can be memcompared until
42+
// the byte specified by the negated return value.
4243
internal virtual unsafe int __GetFieldHelper(int index, out MethodTable* mt)
4344
{
4445
// Value types that don't override this method will use the fast path that looks at bytes, not fields.
4546
Debug.Assert(index == GetNumFields);
4647
mt = default;
47-
return UseFastHelper;
48+
return -(int)this.GetMethodTable()->ValueTypeSize;
49+
}
50+
51+
private unsafe int GetValueTypeSize(int numFields)
52+
{
53+
Debug.Assert(numFields < 0);
54+
int valueTypeSize = -numFields;
55+
Debug.Assert(valueTypeSize <= (int)this.GetMethodTable()->ValueTypeSize);
56+
57+
return valueTypeSize;
4858
}
4959

5060
public override unsafe bool Equals([NotNullWhen(true)] object? obj)
@@ -57,14 +67,13 @@ public override unsafe bool Equals([NotNullWhen(true)] object? obj)
5767
ref byte thisRawData = ref this.GetRawData();
5868
ref byte thatRawData = ref obj.GetRawData();
5969

60-
if (numFields == UseFastHelper)
70+
if (numFields < 0)
6171
{
6272
// Sanity check - if there are GC references, we should not be comparing bytes
6373
Debug.Assert(!this.GetMethodTable()->ContainsGCPointers);
6474

6575
// Compare the memory
66-
int valueTypeSize = (int)this.GetMethodTable()->ValueTypeSize;
67-
return SpanHelpers.SequenceEqual(ref thisRawData, ref thatRawData, valueTypeSize);
76+
return SpanHelpers.SequenceEqual(ref thisRawData, ref thatRawData, GetValueTypeSize(numFields));
6877
}
6978
else
7079
{
@@ -100,10 +109,14 @@ public override unsafe int GetHashCode()
100109

101110
int numFields = __GetFieldHelper(GetNumFields, out _);
102111

103-
if (numFields == UseFastHelper)
104-
hashCode.AddBytes(GetSpanForField(this.GetMethodTable(), ref this.GetRawData()));
112+
if (numFields < 0)
113+
{
114+
hashCode.AddBytes(new ReadOnlySpan<byte>(ref this.GetRawData(), GetValueTypeSize(numFields)));
115+
}
105116
else
117+
{
106118
RegularGetValueTypeHashCode(ref hashCode, ref this.GetRawData(), numFields);
119+
}
107120

108121
return hashCode.ToHashCode();
109122
}

src/coreclr/tools/Common/TypeSystem/IL/Stubs/ComparerIntrinsics.cs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections;
56
using System.Collections.Generic;
67

@@ -240,9 +241,17 @@ private static TypeDesc[] GetPotentialComparersForTypeCommon(TypeDesc type, stri
240241
}
241242

242243
public static bool CanCompareValueTypeBits(MetadataType type, MethodDesc objectEqualsMethod)
244+
{
245+
return CanCompareValueTypeBitsUntilOffset(type, objectEqualsMethod, out int lastFieldOffset)
246+
&& lastFieldOffset == type.InstanceFieldSize.AsInt;
247+
}
248+
249+
public static bool CanCompareValueTypeBitsUntilOffset(MetadataType type, MethodDesc objectEqualsMethod, out int lastFieldEndOffset)
243250
{
244251
Debug.Assert(type.IsValueType);
245252

253+
lastFieldEndOffset = 0;
254+
246255
if (type.ContainsGCPointers)
247256
return false;
248257

@@ -260,6 +269,8 @@ public static bool CanCompareValueTypeBits(MetadataType type, MethodDesc objectE
260269
if (field.IsStatic)
261270
continue;
262271

272+
lastFieldEndOffset = Math.Max(lastFieldEndOffset, field.Offset.AsInt + field.FieldType.GetElementSize().AsInt);
273+
263274
if (!overlappingFieldTracker.TrackField(field))
264275
{
265276
// This field overlaps with another field - can't compare memory
@@ -299,7 +310,7 @@ public static bool CanCompareValueTypeBits(MetadataType type, MethodDesc objectE
299310
}
300311

301312
// If there are gaps, we can't memcompare
302-
if (result && overlappingFieldTracker.HasGaps)
313+
if (result && overlappingFieldTracker.HasGapsBeforeOffset(lastFieldEndOffset))
303314
result = false;
304315

305316
return result;
@@ -341,16 +352,13 @@ public bool TrackField(FieldDesc field)
341352
return true;
342353
}
343354

344-
public bool HasGaps
355+
public bool HasGapsBeforeOffset(int offset)
345356
{
346-
get
347-
{
348-
for (int i = 0; i < _usedBytes.Length; i++)
349-
if (!_usedBytes[i])
350-
return true;
357+
for (int i = 0; i < offset; i++)
358+
if (!_usedBytes[i])
359+
return true;
351360

352-
return false;
353-
}
361+
return false;
354362
}
355363
}
356364

src/coreclr/tools/Common/TypeSystem/IL/Stubs/ValueTypeGetFieldHelperMethodOverride.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,23 +72,31 @@ public override MethodIL EmitIL(MethodDesc specializedMethod)
7272

7373
private MethodIL EmitILCommon(MethodDesc contextMethod)
7474
{
75-
var owningType = (MetadataType)_owningType.InstantiateAsOpen();
76-
7775
ILEmitter emitter = new ILEmitter();
7876

7977
// Types marked as InlineArray aren't supported by
8078
// the built-in Equals() or GetHashCode().
81-
if (owningType.IsInlineArray)
79+
if (_owningType.IsInlineArray)
8280
{
8381
var stream = emitter.NewCodeStream();
8482
MethodDesc thrower = Context.GetHelperEntryPoint("ThrowHelpers", "ThrowNotSupportedInlineArrayEqualsGetHashCode");
8583
stream.EmitCallThrowHelper(emitter, thrower);
8684
return emitter.Link(this);
8785
}
8886

87+
if (_owningType.IsValueType && ComparerIntrinsics.CanCompareValueTypeBitsUntilOffset(_owningType, Context.GetWellKnownType(WellKnownType.Object).GetMethod("Equals", null), out int lastFieldEndOffset))
88+
{
89+
var stream = emitter.NewCodeStream();
90+
stream.EmitLdc(-lastFieldEndOffset);
91+
stream.Emit(ILOpcode.ret);
92+
return emitter.Link(this);
93+
}
94+
8995
TypeDesc methodTableType = Context.SystemModule.GetKnownType("Internal.Runtime", "MethodTable");
9096
MethodDesc methodTableOfMethod = methodTableType.GetKnownMethod("Of", null);
9197

98+
var owningType = (MetadataType)_owningType.InstantiateAsOpen();
99+
92100
ILToken rawDataToken = owningType.IsValueType ? default :
93101
emitter.NewToken(Context.SystemModule.GetKnownType("System.Runtime.CompilerServices", "RawData").GetKnownField("Data"));
94102

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectDataInterner.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
using ILCompiler.DependencyAnalysis;
88

9+
using Internal.IL.Stubs;
910
using Internal.TypeSystem;
1011

1112
using Debug = System.Diagnostics.Debug;
@@ -26,8 +27,16 @@ public ObjectDataInterner(bool genericsOnly)
2627

2728
public bool CanFold(MethodDesc method)
2829
{
29-
return this != Null
30-
&& (!_genericsOnly || method.HasInstantiation || method.OwningType.HasInstantiation);
30+
if (this == Null)
31+
return false;
32+
33+
if (!_genericsOnly || method.HasInstantiation || method.OwningType.HasInstantiation)
34+
return true;
35+
36+
if (method.GetTypicalMethodDefinition() is ValueTypeGetFieldHelperMethodOverride)
37+
return true;
38+
39+
return false;
3140
}
3241

3342
private void EnsureMap(NodeFactory factory)

0 commit comments

Comments
 (0)