Search code examples
c#.netbyteshort

Int16 - bytes capacity in.net?


Why does :

short a=0;
Console.Write(Marshal.SizeOf(a));

shows 2

But if I see the IL code i see :

/*1*/   IL_0000:  ldc.i4.0    
/*2*/   IL_0001:  stloc.0     
/*3*/   IL_0002:  ldloc.0     
/*4*/   IL_0003:  box         System.Int16
/*5*/   IL_0008:  call        System.Runtime.InteropServices.Marshal.SizeOf
/*6*/   IL_000D:  call        System.Console.Write

The LDC at line #1 indicates :

Push 0 onto the stack as int32.

So there must been 4 bytes occupied.

But sizeOf shows 2 bytes...

What am I missing here ? how many byte does short actually take in mem?

I've heard about a situations where there is a padding to 4 bytes so it would be faster to deal with. is it the case also here?

(please ignore the syncRoot and the GC root flag byte i'm just asking about 2 vs 4)


Solution

  • It is pretty easy to tell what's going on by taking a look at the available LDC instructions. Note the limited set of operand types available, there is no version available that load a constant of type short. Just int, long, float and double. These limitations are visible elsewhere, the Opcodes.Add instruction for example is similarly limited, no support for adding variables of one of the smaller types.

    The IL instruction set was very much designed intentionally this way, it reflects the capabilities of a simple 32-bit processor. The kind of processor to think of is the RISC kind, they had their hay-day in the nineteens. Lots of 32-bit cpu registers that can only manipulate 32-bit integers and IEEE-754 floating point types. The Intel x86 core is not a good example, while very commonly used, it is a CISC design that actually supports loading and doing arithmetic on 8-bit and 16-bit operands. But that's more of a historical accident, it made mechanical translation of programs easy that started on the 8-bit 8080 and 16-bit 8086 processors. But such capability doesn't come for free, manipulating 16-bit values actually costs an extra cpu cycle.

    Making IL a good match with 32-bit processor capabilities clearly makes the job of the guy implementing a jitter much simpler. Storage locations can still be a smaller size, but only loads, stores and conversions need to be supported. And only when needed, your 'a' variable is a local variable, one that occupies 32-bits on the stack frame or cpu register anyway. Only stores to memory need to be truncated to the right size.

    There is otherwise no ambiguity in the code snippet. The variable value needs to be boxed because Marshal.SizeOf() takes an argument of type object. The boxed value identifies the type of value by the type handle, it will point to System.Int16. Marshal.SizeOf() has the built-in knowledge to know it takes 2 bytes.

    These restrictions do reflect on the C# language and cause inconsistency. This kind of compile error forever befuddles and annoys C# programmers:

        byte b1 = 127;
        b1 += 1;            // no error
        b1 = b1 + 1;        // error CS0266
    

    Which is a result of the IL restrictions, there is no add operator that takes byte operands. They need to be converted to the next larger compatible type, int in this case. So it works on a 32-bit RISC processor. Now there's a problem, the 32-bit int result needs to be hammered back into a variable that can store only 8-bits. The C# language applies that hammer itself in the 1st assignment but illogically requires a cast hammer in the 2nd assignment.