-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix auto layout algorithm to compute structure alignment correctly #73738
Conversation
- In particular: 1. The alignment requirement imposed by of a non-primitive, non-enum valuetype field is the alignment of that field 2. The alignment requirement imposed by a primitive is the pointer size of the target platform, unless running on Arm32, in which case if the primitive or enum is 8 bytes in size, the alignment requirement is 8. - The previous implementation produced an alignment of pointer size, unless running on Arm32 and one of the fields had an alignment requirement of 8 (in which case the alignment requirement computed for the structure would be 8) In addition, add a test which verifies that the instance field layout test types are actually producing R2R compatible results at runtime. - This test shows that we have some issues around explicit layout, so I was forced to disable that portion of the test for now. Fixes dotnet#65281
} | ||
} | ||
|
||
largestAlignmentRequired = context.Target.GetObjectAlignment(largestAlignmentRequired); | ||
bool requiresAlign8 = !largestAlignmentRequired.IsIndeterminate && largestAlignmentRequired.AsInt > 4; | ||
bool requiresAlign8 = !largestAlignmentRequired.IsIndeterminate && context.Target.PointerSize == 4 && context.Target.GetObjectAlignment(largestAlignmentRequired).AsInt > 4 && context.Target.PointerSize == 4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit - you're checking context.Target.PointerSize == 4
twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, thanks for making another iteration in untangling this non-trivial algorithm!
/azp run runtime-coreclr crossgen2-composite |
Azure Pipelines successfully started running 1 pipeline(s). |
/azp run runtime-coreclr outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
// This does not account for types that are marked IsAlign8Candidate due to 8-byte fields | ||
// but that is explicitly handled when we calculate the final alignment for the type. | ||
|
||
// This behavior is extremely strange for primitive types, as it makes a struct with a single byte in it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on this? Wouldn't struct S { byte x; }
hit IsByValueClass(fieldType)
above and so it would be 1-byte alignment
?
Should this instead say class
since it looks like class C { byte x; }
would hit this case and it would be align: sizeof(void*)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, these questions need David's response but he's on vacation next week. Should we hold off merging this change until he's back so that he can answer them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this targeting .NET 7?
If so then for this one, its just a question on comment wording, I don't think it's worth blocking over.
For the one below, I think it's worth confirming that CG2 is at least matching what the RyuJIT VM does. If it is, then I think its fine to merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment needs clarity, and possibly a test about this particularly odd behavior. From my reading, the rules are that:
struct S1
{
byte b;
}
[StructLayout(LayoutKind.Auto)]
struct S1_Auto
{
byte b;
}
struct S2
{
S1 fld1;
S1_Auto fld2;
}
Assuming 64bit...
This will result in S1
having size of 1, and alignment of 1, S1_Auto
having size of 8 and alignment of 8, and S2
having size of 16, and alignment of 8. Note that this wacky behavior is exclusive to auto layout structures.
Largest alignment rules for classes are less interesting in general, and pretty much just impact ARM32, where a class alignment of 8 is significant, and triggers the 8 byte alignment behavior. However, we still calculate them, in case we add support for 16 byte or higher object alignment rules in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also IsByValueClass(t)
is false for primitive and enum types, and so doesn't go into the structure alignment path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this wacky behavior is exclusive to auto layout structures.
I thought we fixed some of this for generics
so that Nullable<T>
wasn't "too large" anymore? Or was that a different fix since its generic but sequential?
This, more generally, seems like something which may have undesirable consequences and which we may want to look at more in depth for .NET 8. Consider types like ValueTuple
which are frequently used for tuples of primitives and where its marked Auto
with the intent that the BCL optimizes the layout to be more efficient. If the alignment of fields is treated as 8
, that is not going to be the case and many usages will end up more expensive instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm... maybe? I've been writing tests, and to get the wacky layout behavior, I need to make the auto layout structure be larger than a pointer, so the tests that cover these boundary conditions are even dumber than I thought.
if (fieldType.IsPrimitive || fieldType.IsEnum) | ||
{ | ||
// Handle alignment of long/ulong/double on ARM32 | ||
largestAlignmentRequired = LayoutInt.Max(context.Target.GetObjectAlignment(fieldSizeAndAlignment.Size), largestAlignmentRequired); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about Vector128<T>
? Don't we want that to have 16-byte packing (on supported platforms) even if the GC only guarantees 8-byte
alignment?
That would also, seemingly, ensure we have "less changes" to do if the GC ever supports 16-byte alignment in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vector128<T>
uses a different layout algorithm to calculate its alignment. When used as a field of a structure, it will be 16byte aligned within the algorithm with the existing scheme which will result in 16byte alignment for the entire structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, struct S1 { byte x; Vector128<...> y; }
and struct S2 { Vector128<...> x; byte y }
should both be Pack = 16
and therefore size = 32
. Therefore a struct containing S1
or S2
should likewise be Pack = 16
so that the S1/S2
field are at the correct offset.
So shouldn't this factor into largestAlignmentRequired
? If not, could you explain the difference between this and the other algorithm and why they are distinct/separate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's all correct, and does funnel into this algorithm, but the portion you commented on is exclusively for primitives, enums, and pointers (of various types). Normal valuetypes, and the various vector things (and Int128, etc) are not part of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this is the auto layout algorithm, not the sequential layout algorithm. Note, in addition, I'm looking at adding some tests to look at the behavior of classes with fields of higher than pointer alignment, but we'll see where that goes. My suspicion is that the current model for 16 byte structures in class types, in the presence of derived types across R2R version boundaries, does not actually reliably result in aligned 16byte fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But that is irrelevant to the thrust of these changes, which are about allowing higher alignment to affect auto layout fields at all within crossgen2.
.NET 7 backport candidate? |
/backport to release/7.0-rc1 |
Started backporting to release/7.0-rc1: https://github.com/dotnet/runtime/actions/runs/2877629514 |
In particular:
The previous implementation produced an alignment of pointer size, unless running on Arm32 and one of the fields had an alignment requirement of 8 (in which case the alignment requirement computed for the structure would be 8)
In addition, add a test which verifies that the instance field layout test types are actually producing R2R compatible results at runtime.
Fixes #65281