I've created a custom structure for handling RGBA values that will be marshaled to the GPU.
In my type, I am holding individual R, G, B and A components as byte values and am overlapping a 32-bit unsigned integer (Uint32) to easily pass and assign the packed value. I know the concept is obvious but here's a sample of the struct for good measure:
[StructLayout(LayoutKind.Explicit, Size = 4)]
public struct RGBA
{
[FieldOffset(0)]
public uint32 PackedValue;
[FieldOffset(0)]
public byte R;
[FieldOffset(1)]
public byte G;
[FieldOffset(2)]
public byte B;
[FieldOffset(3)]
public byte A;
}
Because of the way c# handles structs, each field must be assigned explicitly in any constructor defined. In my case, that means I have to assign the values twice in any constructors because of the overlapping fields.
I could either use:
public RGBA(uint packed value)
{
R = G = B = A = 0; // initialize all to defaults before assigning packed value
PackedValue = packedValue;
}
public RGBA(byte r, byte g, byte b, byte a)
{
PackedValue = 0; // initialize to default before assigning components
R = r;
G = g;
B = b;
A = a;
}
or I could call the base constructor on each constructor first like so:
public RGBA(uint packedValue) : this()
{
PackedValue = packedValue;
}
public RGBA(byte r, byte g, byte b, byte a) : this()
{
R = r;
G = g;
B = b;
A = a;
}
Since this is for use in graphics code, performance is critical and I'm trying to find the most optimal way to handle constructing in this scenario. Using the first example seems the least overhead of the two examples, since although it involves assigning all fields twice (once for PackedValue, and once for the R, G, B and A fields), the other example involves assigning all values 3 times (twice in the default constructor and once in the defined constructor).
Is there a way to make the compiler recognize that these fields overlap and shouldn't require assigning explicitly R,G,B and A if PackedValue is being assigned and vice-versa? I'm assuming this could be done by manually tweaking the IL generated but I'm wondering if there is a way to handle this more optimally directly in c#.
Any ideas?
From here:
Struct members are automatically initialized to their default values. So, no need to initialize any of them to their default values, in either constructor.
However, that does not apply to your case. It works only for non-overlapped fields and only when you use the default constructor. Anyway, see last part of the answer for an alternative based on this.
Looking at the IL code for the one-param constructor we can see that the compiler does nothing (no optimization, this is release mode with default settings):
.method public hidebysig specialname rtspecialname
instance void .ctor(uint32 packedValue) cil managed
{
// Code size 42 (0x2a)
.maxstack 6
.locals init ([0] uint8 CS$0$0000,
[1] uint8 CS$0$0001,
[2] uint8 CS$0$0002)
IL_0000: ldarg.0
IL_0001: ldarg.0
IL_0002: ldarg.0
IL_0003: ldarg.0
IL_0004: ldc.i4.0
IL_0005: dup
IL_0006: stloc.0
IL_0007: stfld uint8 ConsoleApplication2.Program/RGBA::A
IL_000c: ldloc.0
IL_000d: dup
IL_000e: stloc.1
IL_000f: stfld uint8 ConsoleApplication2.Program/RGBA::B
IL_0014: ldloc.1
IL_0015: dup
IL_0016: stloc.2
IL_0017: stfld uint8 ConsoleApplication2.Program/RGBA::G
IL_001c: ldloc.2
IL_001d: stfld uint8 ConsoleApplication2.Program/RGBA::R
IL_0022: ldarg.0
IL_0023: ldarg.1
IL_0024: stfld uint32 ConsoleApplication2.Program/RGBA::PackedValue
IL_0029: ret
} // end of method RGBA::.ctor
After @usr's suggestion, after jitting it looks the same (this is also release mode, members assigned individually):
007400B5 call 750586FE
007400BA mov eax,dword ptr [ebp-8]
007400BD mov byte ptr [eax],0
G = 0;
007400C0 mov eax,dword ptr [ebp-8]
007400C3 mov byte ptr [eax+1],0
B = 0;
007400C7 mov eax,dword ptr [ebp-8]
007400CA mov byte ptr [eax+2],0
A = 0;
007400CE mov eax,dword ptr [ebp-8]
007400D1 mov byte ptr [eax+3],0
PackedValue = packedValue;
007400D5 mov eax,dword ptr [ebp-8]
007400D8 mov edx,dword ptr [ebp-4]
007400DB mov dword ptr [eax],edx
Perhaps benchmarking it is the best way now. Or, use a default constructor and assign PackedValue
manually after you have the struct instance. In this case the default behavior described in the article will apply.
var rgba = new RGBA { PackedValue = 2556 };
OR
var rgba = new RGBA();
rgba.PackedValue = 2556;
I ran into this problem and ended up using unsafe code to convert my Color struct into a 32 bit integer and back. Not sure if that's an option for you.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With