Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Question about size and padding of a type in C#

Discussion in 'Scripting' started by Long2904, Apr 28, 2022.

  1. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    I have a big class that has a lot of fields in my game. I'm from C with padding in structs and was wondering if the same rule applies to C# as well. For example, in C this struct is 8 bytes large:
    Code (CSharp):
    1. struct A
    2. {
    3.     char a;
    4.     int a;
    5. };
    I was rearranging my class by hand so that the padding is minimum and I calculated that the class would be 144 bytes long. Then I wanted to test if I was doing it correctly and it seemed like Marshal.SizeOf is my best bet (sizeof is required to be inside an unsafe block). I must put a struct layout attribute with layout kind set to sequential on top of my class and the result was... 160 bytes? What? 16 bytes longer than I expected. After some time tinkering with Marshal.OffsetOff, I realized that the GameObject size is 24 bytes long. Why? GameObject is a class so it should be a reference that is only 8 bytes long, right?

    Interested in my new finding, I did a couple more tests:
    Code (CSharp):
    1.  
    2. struct A
    3. {
    4.     public object a;
    5. }
    6.  
    7. struct B
    8. {
    9.     public string b;
    10. }
    11.  
    12. struct C
    13. {
    14.     public object[] c;
    15. }
    16.  
    17. struct D
    18. {
    19.     public GameObject d;
    20. }
    21.  
    22. struct E
    23. {
    24.     public CustomClass e;
    25. }
    26.  
    27. void Test()
    28. {
    29.     Debug.Log(Marshal.SizeOf(typeof(A))); // 0 ???
    30.     Debug.Log(Marshal.SizeOf(typeof(B))); // 8 (My pc is 64 bit so the reference is 8 bytes long)
    31.     Debug.Log(Marshal.SizeOf(typeof(C))); // 8 (make sense, c just holds a reference to an array like b)
    32.     Debug.Log(Marshal.SizeOf(typeof(D))); // 24 ???
    33.     Deubg.Log(Marshal.SizeOf(typeof(E))); // 160 (What? This should be a 8 bytes long reference!)
    34. }
    Maybe I misunderstood the purpose of Marshal.SizeOf? Maybe C# doing padding a little different? Can someone explain to me why is this happening or give me a way to get the size and padding of a type in C#?
     
  2. nijnstein

    nijnstein

    Joined:
    Feb 6, 2021
    Posts:
    78
    The difference is in what sizeof is supposed to return:

    sizeof(type) returns the byte size of a type in memory

    marshall.sizeof returns the byte size needed to marshal the type to unmanaged memory
     
    mopthrow and Bunny83 like this.
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    Right. I would recommend to first look up what Marshalling is and then read about the SizeOf method of the Marshal class. Especially the Remarks section.

    So your usage is misguided here since you most likely don't want to actual marshal your type. Managed "Object" references are non-blitable types..

    The actual managed memory layout can actually differ depending on the CLR and the underlaying architecture. If you want to ensure a certain memory layout for structs, you have to use the StructLayout attribute and define what kind of packing you want (sequential or explicit). This is necessary when you want to exchange the struct with native code. Be careful with explicit layouts. You could accidentally (or on purpose) cause overlaps of fields in memory. This can be used for quite efficient conversion of primitive types (like in my floating point format window).
     
    nijnstein likes this.
  4. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    1. For some reason, sizeof() only works on primitive types and is required to be inside an unsafe block. I don't know if Unity even allows unsafe code or how to turn it on.
    2. I was also just a little surprised that a normal reference (string or array) needs 8 bytes while an object needs 0 and GameObject needs 4. Is that mean the actual GameObject is 4 bytes long or less?
     
  5. VolodymyrBS

    VolodymyrBS

    Joined:
    May 15, 2019
    Posts:
    150
    1. sizeof is limited to unmanaged structs (contains only primitive types). this is by design
    you could download this package https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/, unpack it as zip, and put lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll into you project. is has nice method System.Runtime.CompilerServices.Unsafe.SizeOf which could get size of any struct in managed memory.
    2. as the others already said, Marshall.SizeOf shows how much memory type will use in unmanaged memory, not the space it takes in managed memory. I really recommend you to read what Marshalling is and how its works.
    but in short case by case
    - struct A: Marshall.SizeOf shows 0 because such a struct can not be marshalled and represented in unmanaged memory. runtime does not know how to represent
    object
    in unmanaged memory. BUT it knows how to represent in managed memory.
    object
    is reference type so struct contains only "pointer" so size is 8. and you could check it with Unsafe.SizeOf
    - struct B, struct C: Marshall.SizeOf shows 8 as it just references and both string and array will be marshalled as pointers so the size is 8. same will be in managed memory and Unsafe.SizeOf will show 8 to;
    - struct D : GameObject could be partly marshalled. it contains a few fields that Marshaller know how to marshall. sum of these fields is 24. in managed memory GameObject is just a reference type. so in managed memory struct D takes just 8 bytes. this could be confirmed with Unsafe.SizeOf
    - struct E: can't comment on unmanaged representation as I don't know what is inside CustomClass, but in managed memory struct E will take just 8 bytes because it contains just one reference.
    Code (CSharp):
    1. Debug.Log(Unsafe.SizeOf<A>()); // 8
    2. Debug.Log(Unsafe.SizeOf<B>()); // 8
    3. Debug.Log(Unsafe.SizeOf<C>()); // 8      
    4. Debug.Log(Unsafe.SizeOf<D>()); // 8
    also, be aware that managed runtime could change managed representation in any way it thinks is more optimal. StructLayout attribute control unmanaged representation only. runtime could ignore for managed representation (or could not depending on runtime implementation)
    EDIT: StructLayout attribute control unmanaged representation only for not blittable types types. for blittable types it controls both representations.
    you can find more about blittable types here https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types
     
    Last edited: Apr 28, 2022
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    That's not completely true. As you can read in the documentation link you posted:
     
  7. VolodymyrBS

    VolodymyrBS

    Joined:
    May 15, 2019
    Posts:
    150
    yeah
    yeah my bad. your are right. I forgot to mention that it controls managed and unmanaged structures of blittable types. but my message is still correct for not blittable types (and all structs in the original question are not blittable)

    edited my original post to make it clear
     
    Bunny83 likes this.
  8. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    Thanks for answering! I will check the package out. Just seems kind of weird that you need to do a lot of work just to get the size of a struct. Another thing that I'm wondering: is there any way for me to print out the whole type's layout (padding, offset, and size of individual member)? Maybe some kind of compiler flags or built-in functions? In C you can put -fdump-record-layouts on clang or /d1reportAllClassLayout on msvc.
     
  9. VolodymyrBS

    VolodymyrBS

    Joined:
    May 15, 2019
    Posts:
    150
    the reason is simple - in .NET you do not need to mess with type layout in memory. the only reason to do it is unsafe code (and it's the reason for class name and/or requirement of unsafe context):)


    There are no such functionality because as I said before in C# you do not need to worry about memory layout. runtime do all it thinks is right.
    But if you really want you could do something by using
    Unsafe.AsPointer
    and manually calculating fields offset;
    Code (CSharp):
    1. unsafe void TestStructA()
    2.     {
    3.         var instance = default(TestStruct);
    4.         var aFieldOffset = (long)Unsafe.AsPointer(ref instance.a) - (long)Unsafe.AsPointer(ref instance);
    5.         var bFieldOffset = (long)Unsafe.AsPointer(ref instance.b) - (long)Unsafe.AsPointer(ref instance);
    6.  
    7.         Debug.Log($@"a offset: {aFieldOffset};
    8. b offset: {bFieldOffset};
    9. ");
    10.     }
    11.  
    this will print the layout in managed memory. similar could be done for class too, but it will be trickier.


    BUT the main question is: What do you want to achieve?
    Is not clear to me what do you want to do? why do you care about managed memory layout?
     
    Bunny83 likes this.
  10. VolodymyrBS

    VolodymyrBS

    Joined:
    May 15, 2019
    Posts:
    150
    also you could enable unsafe code with this switch in Project Settings upload_2022-4-29_11-55-36.png
     
    Long2904 likes this.
  11. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    I'm just curious, that's all.
     
    nijnstein and VolodymyrBS like this.