Currently, the C# compiler has a number of definite assignment rules that may not work well with some scenarios or which may require some minimal overhead to work around.
One scenario that comes to mind is when working with union types. Here, the C# compiler doesn't understand that fields may be overlapping, or that they may never be exposed, but it requires you to initialize them anyways.
You can work-around this by taking the address of the type, but that generally involves pinning which can add unnecessary overhead in tight loops.
I propose we expose a method which allows the user to bypass definite assignment rules for a given value. Internally, this would be implemented by taking an out T and immediately returning.
public static partial class Unsafe
{
public static void SkipInit<T>(out T value);
}
Should this be constrained to be unmanaged in order to (outside of various edge cases) prevent users from trying to skip zero-initializing a field to a reference type?
This definitely seems a useful feature, I've had to redundantly default large structs many times in interop code.
Should this be constrained to be unmanaged in order to (outside of various edge cases) prevent users from trying to skip zero-initializing a field to a reference type?
On the one hand, this seems like a nice idea to introduce a little safety to it, but personally I think having it in Unsafe is enough, and I feel like it will just come back to bite some niche usage of this feature where the type is managed but is safely skipping init.
Should this be constrained to be unmanaged in order to (outside of various edge cases) prevent users from trying to skip zero-initializing a field to a reference type?
The JIT will always zero-initialize a field to a reference type.
Talked with @GrabYourPitchforks about this offline, and I'm considering changing the signature to public static void SkipInit<T>(out T source). The implementation would then simply be ret. This should not only be simpler for the JIT to inline, but should achieve the same effect and fit the common use-case better.
Thoughts?
We'd like to see some sample code that not only illustrates how the API is being used but also how they code gen differs.
Related. @GrabYourPitchforks suggested that we should look into making the JIT smarter. @tannergooding said that the analysis for overlapping fields (where is this used) would be expensive. Would be good to validate this with JIT folks.
Example of code that can use this - from https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Decimal.DecCalc.cs#L994:
Buf24 bufNum;
_ = &bufNum; // workaround for CS0165
This would become:
Buf24 bufNum;
Unsafe.SkipInit(out bufNum); // workaround for CS0165
Example makes sense. Thanks.
@dotnet/jit-contrib When we last reviewed this we said it would be nice for the JIT to recognize this pattern and to emit more optimal codegen which avoids double-initialization (zeroing followed by assignment to some fields of the union). However, the theory is that it might be too expensive for the JIT to optimize, and we should instead expose an Unsafe API for folks who need this behavior. Can you speak a bit about whether optimizing this pattern would be a high priority (or even possible at all) for the JIT?
Let me first make sure that I understand the scenario:
s, is fully assigned, but is not recognized as such by the JIT because its fields overlap, or its fields have not been promoted for some other reason.I don't think it would be terribly complicated (implementation-wise) to have the liveness analysis coalesce adjacent partial stores to fields of the same struct (assuming that takes care of the most interesting scenarios), to determine whether it represents a full assignment. However, it would be likely to be somewhat costly. It would require an investigation to validate that (guesses about JIT throughput impact are often wrong!) I'm not sure where this would fall on a priority list, though.
@dotnet/jit-contrib - other thoughts?
The scenario is that, by default you, get 2 initializations of a given struct:
SkipLocalsInitAttributeHowever, when you have a union struct, any byte of memory can be zeroed and/or be required to be initialized 'x' number of more times (depending on how many other fields overlap with that byte).
This proposal gives a "zero-cost" escape hatch to the latter issue (C# definite assignment) by exposing a method that takes an out T, but does nothing and does not aim to address the JIT initialization story (which is handled by SkipLocalsInit).
An alternative to this proposal would be that the JIT needs to recognize an initobj followed by fields being assigned. However, this may be costly to do and may not catch all scenarios. For example, there are many union types for interop code where not all bytes of a struct are always used. It is frequently the case where one variation of the union only uses a subset of the bytes, in which case the JIT would need to decide how to handle the unset bytes.
Ultimately, I think this API provides the easiest balance as it gives a "guaranteed" escape hatch and can give users the ability to completely elide initialization when used in conjunction with SkipLocalsInit.
An alternative to this proposal would be that the JIT needs to recognize an initobj followed by fields being assigned.
Right, that was the alternative I was describing. It involves the JIT understanding that the struct is "fully assigned" by a contiguous string of assignments. Of course, it would only cover the scenario where those assignments are contiguous, and would probably require a small amount of additional analysis - the costly part (throughput-wise; I don't think it's hard to implement) would be recognizing and aggregating the contiguous field assignments during liveness analysis.
For example, there are many union types for interop code where not all bytes of a struct are always used. It is frequently the case where one variation of the union only uses a subset of the bytes, in which case the JIT would need to decide how to handle the unset bytes.
This is a more complex scenario, where the JIT would need to determine that the "unused" bytes do not correspond to any of the declared fields. It handles that case for non-overlapping structs; I'm not sure how much more complex it would be for overlapping fields. However, if the scenario includes the case where actual declared fields are not covered by the field assignments, then there's really no way (AFAICT) for the JIT to determine that it would be safe to omit the zero-init.
However, if the scenario includes the case where actual declared fields are not covered by the field assignments, then there's really no way (AFAICT) for the JIT to determine that it would be safe to omit the zero-init.
Right, and this case is actually fairly common. For example, it isn't uncommon to have a struct definition like the following:
public struct D3D12_RESOURCE_BARRIER
{
public D3D12_RESOURCE_BARRIER_TYPE Type;
public D3D12_RESOURCE_BARRIER_FLAGS Flags;
public _Anonymous_e__Union Anonymous;
[StructLayout(LayoutKind.Explicit)]
public struct _Anonymous_e__Union
{
[FieldOffset(0)]
public D3D12_RESOURCE_TRANSITION_BARRIER Transition;
[FieldOffset(0)]
public D3D12_RESOURCE_ALIASING_BARRIER Aliasing;
[FieldOffset(0)]
public D3D12_RESOURCE_UAV_BARRIER UAV;
}
}
public unsafe struct D3D12_RESOURCE_TRANSITION_BARRIER
{
public ID3D12Resource* pResource;
[NativeTypeName("UINT")]
public uint Subresource;
public D3D12_RESOURCE_STATES StateBefore;
public D3D12_RESOURCE_STATES StateAfter;
}
public unsafe struct D3D12_RESOURCE_ALIASING_BARRIER
{
public ID3D12Resource* pResourceBefore;
public ID3D12Resource* pResourceAfter;
}
public unsafe struct D3D12_RESOURCE_UAV_BARRIER
{
public ID3D12Resource* pResource;
}
The Type field tells you which of the three union fields you should be accessing. So, if you were a D3D12_RESOURCE_BARRIER_TYPE_UAV, you would only be accessing one pointer, leaving up to 12 bytes never touched (as there is no field aliasing them under UAV).
The above pattern comes up all the time in interop code; including for things like DirectX, Vulkan, Win32, Xlib, etc
Looks good as proposed, but we should constrain the T to be unamanged.
After discussion with @jkotas we concluded that we don't want any constraint. The JIT will do the right thing. Not even the struct constraint should be there.
C#
public static partial class Unsafe
{
public static void SkipInit<T>(out T value);
}
we should constrain the
Tto be unamanged.
Why? It just makes this method less useful for no good reason. Nothing else on Unsafe is constrained that way.
The reasoning was that this was intended for union type scenarios; and given that generics/reference types can't overlap; it wouldn't apply to them.
However, it would still apply to union types where value types overlap but the reference/generic types do not.
Most helpful comment
Why? It just makes this method less useful for no good reason. Nothing else on Unsafe is constrained that way.