[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[RISC-V][x64] WiP: Passing empty struct fields - #101796

Closed
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing
Closed

[RISC-V][x64] WiP: Passing empty struct fields#101796
tomeksowi wants to merge 70 commits into
dotnet:mainfrom
tomeksowi:empty-struct-passing

Conversation

@tomeksowi

@tomeksowitomeksowi commented May 2, 2024

Copy link
Copy Markdown
Member

This PR should be viewed as proof of concept, it will be upstreamed in smaller chunks so it can be reviewed with some confidence.

RISC-V and LoongArch

Small structs containing one or two fields, at least one of them floating-point, can be passed (or returned) according to hardware FP calling convention: each field occupying one register. The existing implementation worked only for the narrow case when the fields were naturally aligned. However, the ABIs on both platforms when enregistering fields disregard placement hints such as manual alignment, packing attributes, or padding with empty structs (when they are sized 1 byte like in C++ or .NET). This means additional information on field offsets and sizes needs to be passed wherever registers<->memory copying of such structs happens.

RISC-V only: Unlike LoongArch's, RISC-V's ABI does not bound the size of such structs to 16 bytes. This means, among other things, that we can no longer rule out struct's eligibility for passing according to hardware FP calling convention by simply checking size > 16, which is assumed in many places.

System V x86-64

The current implementation barred a struct containing empty struct fields from enregistration. This did not match the System V ABI which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions". It also does not match the behavior of GCC & Clang on Linux.

Part of #84834, cc @dotnet/samsung

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 2, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label May 2, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@clamp03clamp03 added the arch-riscv Related to the RISC-V architecture label May 3, 2024
The current implementation barred a struct containing empty struct fields from enregistration. This did not match the [System V ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) which says "NO_CLASS This class is used as initializer in the algorithms. It will be used for padding and **empty structures** and unions". It also does not match the behavior of GCC & Clang on Linux.
Comment on lines +823 to +828
[StructLayout(LayoutKind.Explicit, Pack=1)]
struct ExplicitFloatLong
{
[FieldOffset(1)] float FieldF;
[FieldOffset(5)] long FieldL;

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I don't think Pack should be necessary. From what I dug in the codebase, it's a known problem:

if (result < 8 && pMT->RequiresAlign8())
{
// If the structure contains 64-bit primitive fields and the platform requires 8-byte alignment for
// such fields then make sure we return at least 8-byte alignment. Note that it's technically possible
// to create unmanaged APIs that take unaligned structures containing such fields and this
// unconditional alignment bump would cause us to get the calling convention wrong on platforms such
// as ARM. If we see such cases in the future we'd need to add another control (such as an alignment
// property for the StructLayout attribute or a marshaling directive attribute for p/invoke arguments)
// that allows more precise control. For now we'll go with the likely scenario.
result = 8;
}

IMHO the fix would be something like:

  • In CalculateSizeAndFieldOffsets amend calculating alignmentRequirement to:
    GCD(min(alignmentRequirement, packingSize), placementInfo->m_offset)
  • Amend this condition to take explicit offset into consideration:
    // For types with layout we drop any 64-bit alignment requirement if the packing size was less than 8
    // bytes (this mimics what the native compiler does and ensures we match up calling conventions during
    // interop).
    // We don't do this for types that are marked as sequential but end up with auto-layout due to containing pointers,
    // as auto-layout ignores any Pack directives.
    if (HasLayout() && (HasExplicitFieldOffsetLayout() || IsManagedSequential()) && GetLayoutInfo()->GetPackingSize() < 8)
    {
    fFieldRequiresAlign8 = false;
    }

But since this doesn't have much to do with empty structs, I'll leave it for now to keep this PR focused.

…alling convention, in ArgIterator::GetNextOffset and RiscV64Classifier
…n buffer; always calculate GetRiscV64PassStructInRegisterFlags in ComputeReturnFlags\(\)
…. We still need to look at GetRiscV64PassStructInRegisterFlags to rule out passing in registers according to hw FP call conv
…o don't calculate GetRiscV64PassStructInRegisterFlags if struct fits in 16 bytes because we don't care whether it's passed in registers according to integer or hardware floating-point calling convention
Rework CallDescrWorkerInternal to do simple register saving into CallDescrWorker::returnValue rather than try to reconstruct the struct there. Reconstruction respecting the actual field layout is handled then by CopyReturnedFpStructFromRegisters.
This fixes most reflection call tests in JIT/Directed/StructABI/StructABI.
…an argument is passed by ref by looking at m_hasArgLocDescForStructInRegs in ArgIteratorTemplate::IsArgPassedByRef()
…ntion to final destination in MethodDescCallSite::CallTargetWorker
Comment on lines +3173 to +3176
#elif defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// On RISC-V/LoongArch struct { struct{} e1,e2,e3; byte b; float f; } is passed in 2 registers so the
// load/store instruction for 'b' needs to be exact in size or it will overlap 'f'.
return seg.GetRegisterType();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share some information about the case this fixes? Given that promoted struct fields are normalize-on-load, this special case should not be necessary unless there is a bug elsewhere in the RISCV64/LA64 backends.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for cases like this:

publicstructEmptyFloatEmpty5Byte
{
publicEmptye;
publicfloatFieldF;
publicEmptye0,e1,e2,e3,e4;
publicsbyteFieldB;
publicstaticEmptyFloatEmpty5ByteGet()
{
returnnewEmptyFloatEmpty5Byte{FieldF=3.14159f,FieldB=-123};
}
publicboolEquals(EmptyFloatEmpty5Byteother)
{
returnFieldF.Equals(other.FieldF)&&FieldB==other.FieldB;
}
}

Homing the argument in the EmptyFloatEmpty5Byte:Equals prolog trashed the this pointer:

IN0016: 000000 addi sp,sp,-48IN0017: 000004 sd fp,32(sp)IN0018: 000008 sd ra,40(sp)IN0019: 00000C addi fp,sp,32IN001a: 000010 sd a0,-8(fp) // store 'this'IN001b: 000014 fsw f10,-20(fp)IN001c: 000018 sw a1,-11(fp) // stomp on 'this'

I saw native compilers home arguments with appropriately-sized stores at original struct offsets so I fixed it the same way.

Full JitDump of EmptyFloatEmpty5Byte:Equals if interested

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think the fix makes sense. The comment doesn't seem fully accurate then; the real problem seems to be that since the enregistered layout does not match the memory layout of the struct, rounding up the size would potentially extend outside the stack slot.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'll reword the comment.

Oh, and please view this PR as a proof of concept. I need to upstream it in smaller chunks so it can be reviewed with some confidence.

Comment on lines +3162 to +3175
static FpStructInRegistersInfo GetRiscV64PassFpStructInRegistersInfoImpl(TypeHandle th)
{
FpStructInRegistersInfo info = {};
int nFields = 0;
if (!FlattenFields(th, 0, info, nFields DEBUG_ARG(0)))
return FpStructInRegistersInfo{};

using namespace FpStruct;
if ((info.flags & (FloatInt | IntFloat)) == 0)
{
LOG((LF_JIT, LL_EVERYTHING, "FpStructInRegistersInfo: struct %s (%u bytes) has no floating fields\n",
(!th.IsTypeDesc() ? th.AsMethodTable() : th.AsNativeValueType())->GetDebugClassName(), th.GetSize()));
return FpStructInRegistersInfo{};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I alluded to in some other PRs we have the notion of significant padding where it can essentially be considered that all the padding of a struct is covered by fields. If you look at the getTypeLayout implementation you can see how it is computed. The JIT takes care to preserve values in bytes not covered by fields in those cases, and the ABI classification will need to do the same.

For example, a structure declaration like

privateunsafestructS{publicfixedbyteFoo[15];publicfloatBar;}

results in underlying metadata that looks like

privateunsafestructS{publicFooStructFoo;publicfloatBar;}[StructLayout(LayoutKind.Sequential,Size=15)]privatestructFooStruct{publicbyteFixedElementField;}

I'm curious how this code ends up classifying a struct like this one for passing.

It seems like RISC-V/LA64 are going to end up with some potentially surprising user behavior here because of these ABI differences when padding is involved, and because it is somewhat ambiguous to the VM/JIT what is (ignorable) padding (or at least not totally obvious to the user).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a log from a similar case in JIT/Directed/StructABI/StructABI/StructABI.cs:

TID 3f2277: FpStructInRegistersInfo: flattening InlineArray1 (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: flattening <Array>e__FixedBuffer (managed, 1 fields)
TID 3f2277: FpStructInRegistersInfo: * found field FixedElementField [0..1), type: Byte
TID 3f2277: FpStructInRegistersInfo: * array has too many elements: 16

So it stops the field flattening because there's too many fields (max 2 fields to get passed according to FP calling convention) and returns an empty FpStructInRegistersInfo which means pass according to integer calling convention where there is no notion of "fields", structs are a lump of bits laid out in registers as in memory. But it this case struct S is bigger than 16 bytes so it's passed by implicit ref.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now the handling for HasImpliedRepeatedField; that might have to be generalized somewhat. What about the following example?

[StructLayout(LayoutKind.Explicit, Size = 20)]
struct S
{
[FieldOffset(0)]
public byte FirstByteOfArray;
[FieldOffset(16)]
public float FloatField;
}

Maybe SysV classification needs some generalization too (I can see it uses HasImpliedRepeatedFields too). Or perhaps it is the significant padding computation in getTypeLayout that is overly conservative.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as expected:

TID 3f849f: FpStructInRegistersInfo: flattening S (managed, 2 fields)
TID 3f849f: FpStructInRegistersInfo: * found field FirstByteOfArray [0..1), type: Byte
TID 3f849f: FpStructInRegistersInfo: * found field FloatField [12..16), type: Single
TID 3f849f: FpStructInRegistersInfo: struct S (16 bytes) can be passed with floating-point calling convention, flags=0x88; IntFloat, sizes={1, 4}, offsets={0, 12}, IntFieldKindMask=Integer

Note: I downsized to 16 bytes as I'm on #103945 branch where the condition has not been relaxed for RISC-V. But I think it answers your question (padding via explicit layout).

There are problems with handling fixed buffers as the comment in HasImpliedRepeatedField implies. But as far as the classification for RISC-V goes I think it's ok.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeLayout considers the above to be equivalent to a struct struct S {public fixed byte Array[16]; public float FloatField; }. So in that view I don't think the RISC-V classification is ok; IIUC it will silently drop parts of the struct that may contain user data when it gets passed as an argument.

The ABI classification here should match what getTypeLayout decides, one way or the other. I'm not sure if it is possible for us to change what it considers significant padding, since that has been in the JIT for a long time now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user write data to e.g. FirstByteOfArray[5] with standard language features?

Using unsafe code. I am not sure of the historical details of how things evolved this way, but I would guess C++/CLI is a large part of it.

Native compilers in general don't consider padding to be preserved while passing, definitely not the RISC-V ABI. That's the whole point of passing a struct according to FP calling convention, as two fields each in one register.

The difference in .NET is simply what we consider to be the discardable padding. We also have discardable padding, like the last 4 bytes of Span<T>. But ExplicitLayout/explicit size automatically promote the padding of the struct to be considered as "must be preserved".
I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

BTW what would be the .NET equivalent for this C/C++ struct?

struct {
char i;
alignas(16) float f;
};

I am not sure we have an equivalent. I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata. @AaronRobinsonMSFT, @jkoritzinsky or @jkotas should know more about the interop story here...

That's news to me:/ I don't see getTypeLayout used for parameter classification neither on System V nor on RISC-V/LoongArch.

I mention getTypeLayout because it has the current source of truth of when we consider padding in structs to be significant/required to be preserved. I do not know if the rules in there could be relaxed; just that changing the rules would be breaking, and that users are potentially relying on the values in padding of such structures to be preserved. So in that sense it becomes a problem if the ABI does not match with these rules.

I believe all of our existing ABIs come with rules that will preserve this padding, and thus we have not needed to pay any special attention to it before. I could be wrong about this on SysV, in which case I would consider it a bug.

Ultimately I think getTypeLayout and the ABI classification need to agree on what padding in a struct is significant and needs to be preserved, but I do not know whether it would be possible to relax the rules in getTypeLayout or not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any rules defined for what padding is considered significant or just whatever is in the code for getTypeLayout? Becuase treating padding as an array does influence the classification for passing so the user would need to be aware of it. The remarks in StructLayout documentation say it's for controlling the layout to pass a type to unmanaged code, i.e. match the layout of the unmanaged type, and it doesn't mention any of it.

I don't think we explicitly document this part of the rules anywhere (and as you can see in #71711, the semantics were not clear even to ourselves for a long time). I wouldn't be surprised if CUSTOMLAYOUT came to exist quite organically when some issue was noticed where the JIT discarded padding that was expected to be preserved.

@tomeksowitomeksowiJun 25, 2024

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of your example test cases have discardable padding since they do not have ExplicitLayout/explicit size, so there I think the ABI classification done here makes sense. But for the example given above things are different, where the ABI classification should essentially be done as if the padding was replaced by explicit char arrays of the right size.

Right, the bulk of this PR is about empty struct fields, I'll leave out the ExplicitLayout test cases until we hammer out what to do with it, then I'll address it in a dedicated PR.

At any rate, the condition for which padding is preservable needs to be communicated loud and clear because it may change how the argument is passed. I think the best course of action would be to specify something like "When <condition for significant padding>, the field with its padding until the next field is treated as a fixed array of type same as the field" and then let the platform ABI decide how to pass a struct with that additional array so we're not inventing a custom ABI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Yes, that sounds about right.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are both .NET structs (like the ones with ExplicitLayout/Size) that are not representable in C/C++; and C/C++ structs (like your example) that are not representable in .NET metadata.

Agree.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-riscvRelated to the RISC-V architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@tomeksowi@jkotas@jakobbotsch@AaronRobinsonMSFT@clamp03