Don't understand the principle of nullable type? I'm going to dig it out

1: Background

1. Story telling

It's been a month since I made up my mind to do a good job in the media. My brother should know that I have produced many articles and more fans. I also try my best to answer every question I ask. Now, basic and profound questions come one after another, but I'm just a rookie. I can't fly any more (┬). Yesterday, a friend of mine was asked by the interviewer But the principle of empty type is not good. So are the interviewers. They can meet each other face-to-face, and they can't give much money. They can't move the principle, which makes both sides embarrassed 😄😄😄.

2: Give me a hoe. I'll dig it out

How to solve this problem? I also talked about in the previous article that there are two compilation processes from C code to machine code, one is the IL code compiled by csc, the other is the native code compiled by jit, so understanding the IL code and native code is the direction we need to study deeply. I will take the figure of that article.

For the convenience of demonstration, I define an int? Type to accept non null and null situations.


        static void Main(string[] args)
        {
            int? num1 = 10;
            int? num2 = null;

            Console.WriteLine("The execution is over!");
            Console.ReadLine();
        }

1. Dig IL code

It's easy to dig the IL code. You can use the ILSPY tool. The generated IL code is as follows:


.method private hidebysig static 
	void Main (
		string[] args
	) cil managed 
{
	// Method begins at RVA 0x2048
	// Code size 36 (0x24)
	.maxstack 2
	.entrypoint
	.locals init (
		[0] valuetype [mscorlib]System.Nullable`1<int32> num1,
		[1] valuetype [mscorlib]System.Nullable`1<int32> num2
	)

	IL_0000: nop
	IL_0001: ldloca.s 0
	IL_0003: ldc.i4.s 10
	IL_0005: call instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
	IL_000a: ldloca.s 1
	IL_000c: initobj valuetype [mscorlib]System.Nullable`1<int32>
	IL_0012: ldstr "The execution is over!"
	IL_0017: call void [mscorlib]System.Console::WriteLine(string)
	IL_001c: nop
	IL_001d: call string [mscorlib]System.Console::ReadLine()
	IL_0022: pop
	IL_0023: ret
} // end of method Program::Main

This IL code is still very easy to understand, much simpler than assembly (┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬.


    {
        static void Main(string[] args)
        {
            //int? num1 = 10;
            //int? num2 = null;

            Nullable<int> num3 = new Nullable<int>(10);
            Nullable<int> num4 = new Nullable<int>();

            Console.WriteLine("The execution is over!");
            Console.ReadLine();
        }

Very simple, how to output num3 and num4? Just go to Console.WriteLine.

Here you must have a question: why does num3 output 10 and num4 output nothing? Ha ha, this is because Nullable ToString() has been rewritten. Let's see what ToString has been rewritten. The code is as follows:

public struct Nullable<T> where T : struct
{
	private bool hasValue;

	internal T value;

	[NonVersionable]
	[__DynamicallyInvokable]
	public Nullable(T value)
	{
		this.value = value;
		hasValue = true;
	}

	[__DynamicallyInvokable]
	public override string ToString()
	{
		if (!hasValue)
		{
			return "";
		}
		return value.ToString();
	}
}

You can see that either the ToString method returns an empty string or the value you inserted in the constructor. This is so simple that the IL code can be dug here.

2. Digging machine code

To see the machine code of num1 and num2, in fact, the memory layout of nullable < T >. Here I use windbg or! clrstack -l to view the thread stack.


     int? num1 = 10;
     int? num2 =null;

0:007> ~0s
ntdll!ZwReadFile+0x14:
00007ffc`ec11aa64 c3              ret
0:000> !clrstack -l
OS Thread Id: 0x5364 (0)
        Child SP               IP Call Site
ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 21]
    LOCALS:
        0x00000018a9dfeaf8 = 0x0000000a00000001
        0x00000018a9dfeaf0 = 0x0000000000000000

00000018a9dfed08 00007ffcd5b66c93 [GCFrame: 00000018a9dfed08] 

From LOCALS, we can see that the contents stored on the thread stacks of num1 and num2 are 0x0000000a000000001 and 0x0000000000000000 respectively, but this value is also quite strange. One is 1 and the other is 0... We dump the address with the dd command.


0:000> dd 0x00000018a9dfeaf8 
00000018`a9dfeaf8  00000001 0000000a a9dfec08 00000018
0:000> dd 0x00000018a9dfeaf0 
00000018`a9dfeaf0  00000000 00000000 00000001 0000000a

There is a hexadecimal value 0000000a in the memory area of num1, which is the decimal 10. What is the preceding 00000001? Don't forget, int? It's grammar sugar. What you're looking at now is nullable < T > ha...

You can see clearly that there are two value type fields in this structure. Naturally, 00000001 is hasValue=true. num2 is easy to understand. Two default values are two 0. 00000000 00000000.

3: There was an unexpected discovery

1. int? Uses more memory than int

If you have a large amount of memory data, you should be careful. Int? Takes up four bytes more than int on x64, that is, twice as much, no matter thread stack or managed heap.

2. Why does bool take up 4 bytes?

<1> Demonstration on thread stack

Some people must be confused. Is bool a byte in C? How do you say it's four bytes? If you ask me, I can only say that from the perspective of windbg, the thread stack of x64 system takes 4 bytes as a unit. If you don't believe it, I'll define the value types of different fields in the code. If you look at the distribution of the thread stack, it's not good. In fact.

           byte b1 = byte.MaxValue;
           byte b2 = byte.MaxValue;
           short b3 = short.MaxValue;
           short b4 = short.MaxValue;
           int b5 = int.MaxValue;
           int b6 = int.MaxValue;

0:000> !clrstack -l
OS Thread Id: 0xa98 (0)
ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 25]
   LOCALS:
       0x000000a8395fedbc = 0x00000000000000ff
       0x000000a8395fedb8 = 0x00000000000000ff
       0x000000a8395fedb4 = 0x0000000000007fff
       0x000000a8395fedb0 = 0x0000000000007fff
       0x000000a8395fedac = 0x000000007fffffff
       0x000000a8395feda8 = 0x000000007fffffff

Then dump the minimum address 0x000000a8395feda8.


0:000> dd 0x000000a8395feda8
000000a8`395feda8  7fffffff 7fffffff 00007fff 00007fff
000000a8`395fedb8  000000ff 000000ff 395feec8 000000a8
000000a8`395fedc8  395fefc8 000000a8 395fee00 000000a8
000000a8`395fedd8  d5b66c93 00007ffc 98e72d30 000001ee
000000a8`395fede8  76504140 00007ffc 00000000 00000000
000000a8`395fedf8  00000000 00007ffc 395feef0 000000a8
000000a8`395fee08  971d0b20 000001ee 00000000 00000000
000000a8`395fee18  d5b66b79 00007ffc 00000000 00000000

By comparison, you can see that 7fffff, 00007fff and 000000ff above are the MaxValue of the corresponding int, short and byte. They all take up 4 bytes of space. No problem.

<2> Managed heap demo


    var arr1 = new int[] { 10 };
    var arr2 = new int?[] { 14 };

0:000> !clrstack -l
OS Thread Id: 0x23f8 (0)
000000859a1fec60 00007ffc76630967 ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 32]
    LOCALS:
        0x000000859a1feca0 = 0x000002773cb32d70
        0x000000859a1fec98 = 0x000002773cb32d90

000000859a1feeb8 00007ffcd5b66c93 [GCFrame: 000000859a1feeb8] 
0:000> !do 0x000002773cb32d70
Name:        System.Int32[]
MethodTable: 00007ffcd2d58538
EEClass:     00007ffcd2ec5918
Size:        28(0x1c) bytes
Array:       Rank 1, Number of elements 1, Type Int32 (Print Array)
Fields:
None
0:000> !do 0x000002773cb32d90
Name:        System.Nullable`1[[System.Int32, mscorlib]][]
MethodTable: 00007ffcd3fb2058
EEClass:     00007ffcd30221a0
Size:        32(0x20) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE (Print Array)
Fields:
None

0:000> !objsize 0x000002773cb32d70
sizeof(000002773cb32d70) = 32 (0x20) bytes (System.Int32[])
0:000> !objsize 0x000002773cb32d90
sizeof(000002773cb32d90) = 32 (0x20) bytes (System.Nullable`1[[System.Int32, mscorlib]][])

As you can see, one is 28byte, the other is 32byte, and the extra one is the hasValue ha. Pay attention to that! objsize is 32byte, because 28byte needs 8 alignment to become 32byte. Then I dump the two value types, as shown in the following figure:

4: Summary

I don't know if I've dug into the blind spot of the interviewer 😄 In a word, int? Is nullable < T >, and it has four bytes more space than non nullable. Finally, you need to use it according to your own situation.

Tags: Programming

Posted on Wed, 13 May 2020 21:37:08 -0400 by morris