Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Seeking optimal way to handle constructors of struct with overlapping fields

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?

like image 396
Mike Johnson Avatar asked Aug 01 '14 21:08

Mike Johnson


2 Answers

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;
like image 181
Marcel N. Avatar answered Nov 04 '22 18:11

Marcel N.


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.

like image 1
jjxtra Avatar answered Nov 04 '22 18:11

jjxtra