What's the difference between sizeof(T) and Unsafe.SizeOf<T>()?
Asked Answered
D

1

38

First of all, a small disclaimer before the actual question:

I know there are a lot of closed/duplicate questions regarding the difference between the sizeof operator and the Marshal.SizeOf<T> method, and I do understand the difference between the two. Here I'm talking about the SizeOf<T> method in the new Unsafe class

So, I'm not sure I understand the actual difference between these two operations, and whether there's a specific difference when using the method on a struct/class in particular.

The sizeof operator takes a Type name and returns the number of managed bytes it is supposed to take up when allocated (ie. an Int32 will return 4, for example).

The Unsafe.SizeOf<T> method on the other hand, is implemented in IL like all the other methods in the Unsafe class, and looking at the code here's what it does:

.method public hidebysig static int32 SizeOf<T>() cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 1
    sizeof !!T
    ret
}

Now, if I'm not wrong, the code is just calling sizeof !!T which is the same as sizeof(T) (calling the sizeof operator with the type name T), so wouldn't the two of them be exactly equivalent?

Also, I see the method is also allocating a useless object (the NonVersionableAttribute) in the first line, so wouldn't that cause a small amount of memory to be heap-allocated as well?

My question is:

Is it safe to say that the two methods are perfectly equivalent and that therefore it is just better to use the classic sizeof operator, as that also avoid the allocation of that attribute in the SizeOf<T> method? Was this SizeOf<T> method added to the Unsafe class just for convenience at this point?

Descartes answered 21/12, 2017 at 10:9 Comment(4)
The .custom statement just tells you that there is a custom-attribute present belonging to this method. As custom-attributes are just metadata they are not involved in a regular method call.Hamitosemitic
Just a clarification, is this a BCL class I am unaware of, or some third party extension? You're right that it doesn't do anything, perhaps the idea was to provide a "hook" for some cross-platform wizardry in the future?Bonnybonnyclabber
@Groo It's a new class that's been added to CoreFX I believe with .NET Core 2.0 (not exactly sure, but it's there in .NET Core 2.0 anyways). See here: learn.microsoft.com/it-it/dotnet/api/…Descartes
This was a necessary hack to implement System.Memory. The coreclr tracker issue is here. A pure implementation detail, the value it returns depends on the jitter being used at runtime. The C# compiler still rejects any attempt to get the size of a non-blittable type.Malda
P
30

While this method indeed just uses sizeof IL instruction - there is a difference with regular sizeof operator, because this operator cannot be applied to arbitrary types:

Used to obtain the size in bytes for an unmanaged type. Unmanaged types include the built-in types that are listed in the table that follows, and also the following:

Enum types

Pointer types

User-defined structs that do not contain any fields or properties that are reference types

If you try to write analog of Unsafe.SizeOf - it will not work:

public static int SizeOf<T>()
{
    // nope, will not compile
    return sizeof(T);
}

So Unsafe.SizeOf lifts restrictions of sizeof operator and allow you to use IL sizeof instruction with arbitrary types (including reference types for which it will return size of reference).

As for attribute construct you see in IL - that does not mean attribute will be instantiated for each call - that's just IL syntax for associating attributes with various members (method in this case).

Examples:

public struct Test {
    public int Int1;
}

static void Main() {
    // works
    var s1 = Unsafe.SizeOf<Test>();
    // doesn't work, need to mark method with "unsafe"
    var s2 = sizeof(Test);            
}

Another example:

public struct Test {
    public int Int1;
    public string String1;
}


static unsafe void Main() {
    // works, return 16 in 64bit process - 4 for int, 4 for padding, because
    // alignment of the type is the size of its largest element, which is 8
    // and 8 for string
    var s1 = Unsafe.SizeOf<Test>();
    // doesn't work even with unsafe, 
    // cannot take size of variable of managed type "Test"
    // because Test contains field of reference type (string)
    var s2 = sizeof(Test);                        
} 
Ploss answered 21/12, 2017 at 10:36 Comment(4)
Perfect, thanks! It's kinda weird though that we need this method to be able to do that, I wonder why they chose not to update the C# compiler to let the sizeof operator do the same without that additional IL method. Also, wait a second, shouldn't an int (as it's an Int32 struct) always be 4-bytes long, even i 64-bits processes? Shouldn't the size of Test be 12 bytes?Descartes
@Descartes yes sure, int is 4 bytes and another 4 bytes is padding, fixed that. As for why not change sizeof operator - I don't know exactly of course, but I guess this requires change to compiler and Unsafe.SizeOf doesn't require any changes. Plus, there was a reason in the first place to make sizeof work like that, so I suppose you need to really know what you are doing when using Unsafe.SizeOf (it's unsafe after all).Ploss
Makes sense, thanks for the additional explanation! Totally forgot about the field offset alignment, it's been a while since I last coded in C and you tend to forget about these tiny details when using a managed language ahahahahDescartes
@Descartes It's called Unsafe for a reason. Better have a safe language and possibly unsafe API than an unsafe language alone.Asymmetry

© 2022 - 2024 — McMap. All rights reserved.