Does structs generate inaccessible copies?

Consider the following large structure for the asked questions
[InlineArray(128)]
public struct x1024BitBuffer
{
byte e0;
}
[InlineArray(128)]
public struct x1024BitBuffer
{
byte e0;
}
Does a local copy is created when returning from a method? In my understanding of how structure returning methods are handled, the current method will allocate the necessary memory to store the called method's return within the stack. But, the called method also has it's own stack, so, in the next scenarios, does the struct gets copied back in forth or is it internally passed by reference?
// consider that we marked this method NOT to be inlined
static x1024BitBuffer CreateMe()
=> new()
;

static void Main()
{
var x = CreateMe();
}
// consider that we marked this method NOT to be inlined
static x1024BitBuffer CreateMe()
=> new()
;

static void Main()
{
var x = CreateMe();
}
When Main invokes CreateMe is the structure allocated inside the creator method or does the compiler detects that no one can access it so it makes new() allocate directly into x's var memory? What if the variable is accessed in the creator method but only returned
// consider that we marked this method NOT to be inlined
static x1024BitBuffer CreateMe()
{
var temp = new x1024BitBuffer ();
temp[5] = 0x11;
return temp;
}

static void Main()
{
var x = CreateMe();
}
// consider that we marked this method NOT to be inlined
static x1024BitBuffer CreateMe()
{
var temp = new x1024BitBuffer ();
temp[5] = 0x11;
return temp;
}

static void Main()
{
var x = CreateMe();
}
Since the value temp is only modified and then returned, is the memory for it allocated in CreateMe's stack and then copied to the caller method stack or accessed directly? Does a a copy of a struct is created when passing through an init accessor?
readonly struct Data
{
readonly x1024BitBuffer payload;

public x1024BitBuffer Payload
{
init => payload = value;
}
}

static void Main()
{
Data data = new()
{
PayLoad = CreateMe()
};
}
readonly struct Data
{
readonly x1024BitBuffer payload;

public x1024BitBuffer Payload
{
init => payload = value;
}
}

static void Main()
{
Data data = new()
{
PayLoad = CreateMe()
};
}
When creating a Data instance, the IL method init_Payload is called. So the stack traces looks something like the following .With that in mind, does the large structure gets copied 2 times or the compiler optmises that in some way? Main() -> CreateMe() -> init_Payload new Data() | => new(); | arg: value
6 Replies
๐—๐—ฎ๐—บ๐—ฒ๐˜€
Found this article that reinforces the usage of fields instead of properties in these specific cases discussed in it as being one of possible solutions to reduce copy overhead https://devblogs.microsoft.com/premier-developer/the-in-modifier-and-the-readonly-structs-in-c/
Sergey Tepliakov
Developer Support
The โ€˜inโ€™-modifier and the readonly structs in C# - Developer Suppor...
C# 7.2 got two very important features for high-performance scenarios โ€” the readonly structs and the in parameters. But to understand why this additions are so important and how theyโ€™re related to each other we should look back in history. As you probably know, the .NET ecosystem has two family of types โ€” the value [โ€ฆ]
sibber
sibberโ€ข3mo ago
try #allow-unsafe-blocks
๐—๐—ฎ๐—บ๐—ฒ๐˜€
@sibber ๐Ÿ‡ต๐Ÿ‡ธ ยฏ\_(ใƒ„)_/ยฏ
canton7
canton7โ€ข3mo ago
Does a local copy is created when returning from a method?
Looks like the caller allocates it. I'm not sure what the guarantees are there, if any. In practice a small method like that will be inlined, so it's moot. https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgAYBYAKDIEYbjyACYugOgCUBXAOwwCW+GOwDCEfAAcBAGxhQAyvIBuAsDFwBuGjuoBtAJK8ZA3jACCUKNgCeACjqkAHAEoAuowDMzXBijcwDGYEOnJSFAAhAQwI7gAzOPlmGgBvGmZmYBsMGGYYcm1qAF9dYm8yZlFU9IzmPQBZGAwACwgAEwMpGTtGlvbOyRkAeUlBCF5cdgA5CCMTXlMAc3cajLYkYNDwqJj4xKhK2GwcxrsXZgBeAD5mMwB3M8LV1joN4hRmeuxTM+e06lqtWU2AOCEuhxgxxgpxchUBJWKQA==
What if the variable is accessed in the creator method but only returned
Same: https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgAYBYAKDIEYbjyACYugOgCUBXAOwwCW+GOwDCEfAAcBAGxhQAyvIBuAsDFwBuGjuoBtAJK8ZA3jACCUKNgCeACjqkAHAEoAuowDMzXBijcwDGYEOnJSFAAhAQwI7gAzOPlmGgBvGmZmYBsMGGYYcm1qAF9dYm8yZlFU9IzmPQBZGAwACwgAEwMpGTtGlvbOyRkAeUlBCF5cdgA5CCMTXlMAc3cajLYkYNDwqJj4xKhK2GwcxrsXVeY06lra5WwDnKlmAF5mMwB3TbDI6NiEpLOhRuGUekj0AFY3C9mOQQnQgcDiAB2ZighEZErUXQ3dasFDMerYUxnC5XYHMO4HBDQ0RHE4wQEXTFFIA==
What if the variable is accessed in the creator method but only returned
In the init method, it's passed a pointer to the struct (a struct that large can't be passed in registers sensibly), but it then looks like it makes a defensive copy. It looks like Main is doing a bunch of additional copying too, although I haven't spent enough time staring at the assembly to figure out what. https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgAYBYAKDIEYbjyACYugOgCUBXAOwwCW+GOwDCEfAAcBAGxhQAyvIBuAsDFwBuGjuoBtAJK8ZA3jACCUKNgCeACjqkAHAEoAuowDMzXBijcwDGYEOnJSFAAhAQwI7gAzOPlmGgBvGmZmYBsMGGYYcm1qAF9dYm8yZlFU9IzmPQBZGAwACwgAEwMpGTtGlvbOyRkAeUlBCF5cdgA5CCMTXlMAc3cajLYkYNDwqJj4xKhK2GwcxrsXZgBeAD5mMwB3M8La5Opn9dYUZnrsUzOX57Sr2ezAAIsdsMw2uDLrcYA8XKtaoDgc8AAq2GQQbBtGGiI4nGBnREZIpPWolYq6I5tcYyGw+PwBIJgjDYapA6m0+khMKRaKxBJJSQYrFtMk1MqbXk7AX7ZjomyY7H/ZjI2oNJqtDpdHqa/pdEZjCbTWbGUxLFZAjLmoLXZjCxWimHKbAybgwMkUik0IA==
๐—๐—ฎ๐—บ๐—ฒ๐˜€
@canton7, thanks for the return. I'm not really educated on reading those, can you enlighten more about what is happening in the JIT behind the scenes. Im trying to figure out practices to avoid Inacessible copies when handling structs in methods and stuff but as far as i understood, the only certain way is to pass everything ref
canton7
canton7โ€ข3mo ago
In the first one: sub rsp, 0xa8 allocates 0xa8 bytes on the stack (by subtracting 0xa8 from rsp, the stack pointer). That's then copied to rcx: under the calling convention which 64-bit Windows uses, the first integer argument to a function is passed in rcx, so this pointer to the space to store the return value is being passed as a silent first argument. CreateMe when zeros the memory pointed to by rcx (using those SIMD instructions to set lots of bytes at once). So the caller is allocating stack space, passing a pointer to that space into CreateMe, and CreateMe is initialising it. CreateMe isn't creating a local copy first. The second one is pretty much identical. In the third one: Main this time allocates 0x128 bytes of stack (sub rsp, 0x128). My x64 assembly isn't great, but it looks like it's doing the new Data() bit by zeroing out a lot of that stack, then, it calls CreateMe() (which works the same as before: caller passes a pointer to stack space, callee initialises it). With both data and the x1024BitBuffer instances next to each other on the stack, it calls into set_Payload, which copies the x1024BitBuffer instance into the field in data. With structs, you need to bear in mind the rules for when defensive copied have to be made, and the things which influence that (readonly, etc). But, also bear in mind that it took only around 4 instructions to initialise your 128-byte array, and 8 instructions to copy it. Each of those vmovdqu instructions has a latency of 1, so this isn't a huge cost
Want results from more Discord servers?
Add your server