用了這麼多年的泛型,你對它到底有多瞭解?

現代程序員寫代碼沒有人敢說自己沒用過泛型,這個泛型模板T可以被任何你想要的類型替代,確實很魔法很神奇,很多人也習以為常了,但就是這麼有趣的泛型T底層到底是怎麼幫你實現的,不知道有多少人清楚底層玩法,這篇我就試著來分享一下,不一定全對哈。。。

一:沒有泛型前

現在的netcore 3.1和最新的.netframework8早已經沒有當初那個被人詬病的ArrayList了,但很巧這玩意不得不說,因為它決定了C#團隊痛改前非,拋棄過往重新上路,上一段ArrayList案例代碼。

<code>    public class ArrayList
    {
        private object[] items;

        private int index = 0;

        public ArrayList()
        {
            items = new object[10];
        }

        public void Add(object item)
        {
            items[index++] = item;
        }
    }
/<code>

上面這段代碼,為了保證在Add中可以塞入各種類型 eg: int,double,class, 就想到了一個絕招用祖宗類object接收,這就引入了兩大問題,裝箱拆箱和類型安全。

1. 裝箱拆箱

這個很好理解,因為你使用了祖宗類,所以當你 Add 的時候塞入的是值類型的話,自然就有裝箱操作,比如下面代碼:

<code>            ArrayList arrayList = new ArrayList();
            arrayList.Add(3);
/<code>

<1> 佔用更大的空間

這個問題我準備用windbg看一下,相信大家知道一個int類型佔用4個字節,那裝箱到堆上是幾個字節呢,好奇吧。

原始代碼和IL代碼如下:

<code>        public static void Main(string[] args)
        {
            var num = 10;
            var obj = (object)num;
            Console.Read();
        }

    IL_0000: nop
	IL_0001: ldc.i4.s 10
	IL_0003: stloc.0
	IL_0004: ldloc.0
	IL_0005: box [mscorlib]System.Int32
	IL_000a: stloc.1
	IL_000b: call int32 [mscorlib]System.Console::Read()
	IL_0010: pop
	IL_0011: ret

/<code>

可以清楚的看到IL_0005 中有一個box指令,裝箱沒有問題,然後抓一下dump文件。

~0s -> !clrstack -l -> !do 0x0000018300002d48

<code>
0:000> ~0s
ntdll!ZwReadFile+0x14:
00007ff9`fc7baa64 c3              ret
0:000> !clrstack -l
OS Thread Id: 0xfc (0)
        Child SP               IP Call Site
0000002c397fedf0 00007ff985c808f3 ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 28]
    LOCALS:
        0x0000002c397fee2c = 0x000000000000000a
        0x0000002c397fee20 = 0x0000018300002d48

0000002c397ff038 00007ff9e51b6c93 [GCFrame: 0000002c397ff038] 
0:000> !do 0x0000018300002d48
Name:        System.Int32
MethodTable: 00007ff9e33285a0
EEClass:     00007ff9e34958a8
Size:        24(0x18) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ff9e33285a0  40005a0        8         System.Int32  1 instance               10 m_value

/<code>

倒數第5行 Size: 24(0x18) bytes, 可以清楚的看到是24字節。 為什麼是24個字節,8(同步塊指針) + 8(方法表指針) + 4(對象大小)=20,但因為是x64位,內存是按8對齊,也就是要按8的倍數計算,所以佔用是 8+8+8 =24 字節,原來只有4字節的大小因為裝箱已被爆到24字節,如果是10000個值類型的裝箱那空間佔用是不是挺可怕的?

<2> 棧到堆的裝箱搬運到運輸到售後到無害化處理都需要付出重大的人力和機器成本

2. 類型不安全

很簡單,因為是祖宗類型object,所以無法避免程序員使用亂七八糟的類型,當然這可能是無意的,但是編譯器確無法規避,代碼如下:

<code>
            ArrayList arrayList = new ArrayList();
            arrayList.Add(3);
            arrayList.Add(new Action((num) => { }));
            arrayList.Add(new object());

/<code>

面對這兩大尷尬的問題,C#團隊決定重新設計一個類型,實現一定終身,這就有了泛型。

二:泛型的出現

1. 救世主

首先可以明確的說,泛型就是為了解決這兩個問題而生的,你可以在底層提供的List中使用List,List。。。等等你看得上的類型,而這種技術的底層實現原理才是本篇關注的重點。

<code>
        public static void Main(string[] args)
        {
            List list1 = new List();
            List list3 = new List();
            ...
        }

/<code>

三:泛型原理探究

這個問題的探索其實就是 List -> List在何處實現了 T -> int 的替換,反觀java,它的泛型實現其實在底層還是用object來替換的,C#肯定不是這麼做的,不然也沒這篇文章啦,要知道在哪個階段被替換了,你起碼要知道C#代碼編譯的幾個階段,為了理解方便,我畫一張圖吧。

用了這麼多年的泛型,你對它到底有多瞭解?

流程大家也看到了,要麼在MSIL中被替換,要麼在JIT編譯中被替換。。。

<code>
        public static void Main(string[] args)
        {
            List list1 = new List();
            List list2 = new List();
            List list3 = new List();
            List list4 = new List();

            Console.ReadLine();
        }

/<code>

1. 在第一階段探究

因為第一階段是MSIL代碼,所以用ILSpy看一下中間代碼即可。

<code>		IL_0000: nop
		IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1::.ctor()
		IL_0006: stloc.0
		IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1::.ctor()
		IL_000c: stloc.1
		IL_000d: newobj instance void class [mscorlib]System.Collections.Generic.List`1::.ctor()
		IL_0012: stloc.2
		IL_0013: newobj instance void class [mscorlib]System.Collections.Generic.List`1::.ctor()
		IL_0018: stloc.3
		IL_0019: call string [mscorlib]System.Console::ReadLine()
		IL_001e: pop
		IL_001f: ret

.class public auto ansi serializable beforefieldinit System.Collections.Generic.List`1
	extends System.Object
	implements class System.Collections.Generic.IList`1,
	           class System.Collections.Generic.ICollection`1,
	           class System.Collections.Generic.IEnumerable`1,
	           System.Collections.IEnumerable,
	           System.Collections.IList,
	           System.Collections.ICollection,
	           class System.Collections.Generic.IReadOnlyList`1,
	           class System.Collections.Generic.IReadOnlyCollection`1

/<code>

從上面的IL代碼中可以看到,最終的類定義還是 System.Collections.Generic.List1\,說明在中間代碼階段還是沒有實現 T -> int 的替換。

2. 在第二階段探究

想看到JIT編譯後的代碼,這個說難也不難,其實每個對象頭上都有一個方法表指針,而這個指針指向的就是方法表,方法表中有該類型的所有最終生成方法,如果不好理解,我就畫個圖。

用了這麼多年的泛型,你對它到底有多瞭解?

!dumpheap -stat 尋找託管堆上的四個List對象。

<code>
0:000> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ff9e3314320        1           32 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
00007ff9e339b4b8        1           40 System.Collections.Generic.List`1[[System.Double, mscorlib]]
00007ff9e333a068        1           40 System.Collections.Generic.List`1[[System.Int32, mscorlib]]
00007ff9e3330d58        1           40 System.Collections.Generic.List`1[[System.String, mscorlib]]
00007ff9e3314a58        1           40 System.IO.Stream+NullStream
00007ff9e3314510        1           40 Microsoft.Win32.Win32Native+InputRecord
00007ff9e3314218        1           40 System.Text.InternalEncoderBestFitFallback
00007ff985b442c0        1           40 System.Collections.Generic.List`1[[System.Int32[], mscorlib]]
00007ff9e338fd28        1           48 System.Text.DBCSCodePageEncoding+DBCSDecoder
00007ff9e3325ef0        1           48 System.SharedStatics

/<code>

可以看到從託管堆中找到了4個list對象,現在我就挑一個最簡單的 System.Collections.Generic.List1[[System.Int32, mscorlib]] ,前面的 00007ff9e333a068 就是方法表地址。

!dumpmt -md 00007ff9e333a068

<code>
0:000> !dumpmt -md 00007ff9e333a068
EEClass:         00007ff9e349b008
Module:          00007ff9e3301000
Name:            System.Collections.Generic.List`1[[System.Int32, mscorlib]]
mdToken:         00000000020004af
File:            C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
BaseSize:        0x28
ComponentSize:   0x0
Slots in VTable: 77
Number of IFaces in IFaceMap: 8
--------------------------------------
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e3882450 00007ff9e3308de8 PreJIT System.Object.ToString()
00007ff9e389cc60 00007ff9e34cb9b0 PreJIT System.Object.Equals(System.Object)
00007ff9e3882090 00007ff9e34cb9d8 PreJIT System.Object.GetHashCode()
00007ff9e387f420 00007ff9e34cb9e0 PreJIT System.Object.Finalize()
00007ff9e38a3650 00007ff9e34dc6e8 PreJIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32)
00007ff9e4202dc0 00007ff9e34dc7f8 PreJIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Insert(Int32, Int32)

/<code>

上面方法表中的方法過多,我做了一下刪減,可以清楚的看到,此時Add方法已經接受(Int32)類型的數據了,說明在JIT編譯之後,終於實現了 T -> int 的替換,然後再把 List 打出來看一下。

<code>
0:000> !dumpmt -md 00007ff9e339b4b8
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e3882450 00007ff9e3308de8 PreJIT System.Object.ToString()
00007ff9e389cc60 00007ff9e34cb9b0 PreJIT System.Object.Equals(System.Object)
00007ff9e3882090 00007ff9e34cb9d8 PreJIT System.Object.GetHashCode()
00007ff9e387f420 00007ff9e34cb9e0 PreJIT System.Object.Finalize()
00007ff9e4428730 00007ff9e34e4170 PreJIT System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double)
00007ff9e3867a00 00007ff9e34e4280 PreJIT System.Collections.Generic.List`1[[System.Double, mscorlib]].Insert(Int32, Double)

/<code>

上面看的都是值類型,接下來再看一下如果 T 是引用類型會是怎麼樣呢?

<code>
0:000> !dumpmt -md 00007ff9e3330d58
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e3890060 00007ff9e34eb058 PreJIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)

0:000> !dumpmt -md 00007ff985b442c0
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e3890060 00007ff9e34eb058 PreJIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)

/<code>

可以看到當是List 和 List 的時候,JIT使用了 System.__Canon 這麼一個類型作為替代,有可能人家是攝影愛好者吧,為什麼用__Canon替代引用類型,這是因為它想讓能共享代碼區域的方法都共享來節省空間和內存吧,不信的話可以看看它們的Entry列都是同一個內存地址:00007ff9e3890060, 打印出來就是這麼一段彙編。

<code>
0:000> !u 00007ff9e3890060
preJIT generated code
System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)
Begin 00007ff9e3890060, size 4a
>>> 00007ff9`e3890060 57              push    rdi
00007ff9`e3890061 56              push    rsi
00007ff9`e3890062 4883ec28        sub     rsp,28h
00007ff9`e3890066 488bf1          mov     rsi,rcx
00007ff9`e3890069 488bfa          mov     rdi,rdx
00007ff9`e389006c 8b4e18          mov     ecx,dword ptr [rsi+18h]
00007ff9`e389006f 488b5608        mov     rdx,qword ptr [rsi+8]
00007ff9`e3890073 3b4a08          cmp     ecx,dword ptr [rdx+8]
00007ff9`e3890076 7422            je      mscorlib_ni+0x59009a (00007ff9`e389009a)
00007ff9`e3890078 488b4e08        mov     rcx,qword ptr [rsi+8]
00007ff9`e389007c 8b5618          mov     edx,dword ptr [rsi+18h]
00007ff9`e389007f 448d4201        lea     r8d,[rdx+1]
00007ff9`e3890083 44894618        mov     dword ptr [rsi+18h],r8d
00007ff9`e3890087 4c8bc7          mov     r8,rdi
00007ff9`e389008a ff152088faff    call    qword ptr [mscorlib_ni+0x5388b0 (00007ff9`e38388b0)] (JitHelp: CORINFO_HELP_ARRADDR_ST)
00007ff9`e3890090 ff461c          inc     dword ptr [rsi+1Ch]
00007ff9`e3890093 4883c428        add     rsp,28h
00007ff9`e3890097 5e              pop     rsi
00007ff9`e3890098 5f              pop     rdi
00007ff9`e3890099 c3              ret
00007ff9`e389009a 8b5618          mov     edx,dword ptr [rsi+18h]
00007ff9`e389009d ffc2            inc     edx
00007ff9`e389009f 488bce          mov     rcx,rsi
00007ff9`e38900a2 90              nop
00007ff9`e38900a3 e8c877feff      call    mscorlib_ni+0x577870 (00007ff9`e3877870) (System.Collections.Generic.List`1[[System.__Canon, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5)
00007ff9`e38900a8 ebce            jmp     mscorlib_ni+0x590078 (00007ff9`e3890078)

/<code>

然後再回過頭看List 和 List ,從Entry列中看確實不是一個地址,說明List 和 List 是兩個完全不一樣的Add方法,看得懂彙編的可以自己看一下哈。。。

<code>MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e38a3650 00007ff9e34dc6e8 PreJIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32)
00007ff9e4428730 00007ff9e34e4170 PreJIT System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double)

0:000> !u 00007ff9e38a3650
preJIT generated code
System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32)
Begin 00007ff9e38a3650, size 50
>>> 00007ff9`e38a3650 57              push    rdi
00007ff9`e38a3651 56              push    rsi
00007ff9`e38a3652 4883ec28        sub     rsp,28h
00007ff9`e38a3656 488bf1          mov     rsi,rcx
00007ff9`e38a3659 8bfa            mov     edi,edx
00007ff9`e38a365b 8b5618          mov     edx,dword ptr [rsi+18h]
00007ff9`e38a365e 488b4e08        mov     rcx,qword ptr [rsi+8]
00007ff9`e38a3662 3b5108          cmp     edx,dword ptr [rcx+8]
00007ff9`e38a3665 7423            je      mscorlib_ni+0x5a368a (00007ff9`e38a368a)
00007ff9`e38a3667 488b5608        mov     rdx,qword ptr [rsi+8]
00007ff9`e38a366b 8b4e18          mov     ecx,dword ptr [rsi+18h]
00007ff9`e38a366e 8d4101          lea     eax,[rcx+1]
00007ff9`e38a3671 894618          mov     dword ptr [rsi+18h],eax
00007ff9`e38a3674 3b4a08          cmp     ecx,dword ptr [rdx+8]
00007ff9`e38a3677 7321            jae     mscorlib_ni+0x5a369a (00007ff9`e38a369a)
00007ff9`e38a3679 4863c9          movsxd  rcx,ecx
00007ff9`e38a367c 897c8a10        mov     dword ptr [rdx+rcx*4+10h],edi
00007ff9`e38a3680 ff461c          inc     dword ptr [rsi+1Ch]
00007ff9`e38a3683 4883c428        add     rsp,28h
00007ff9`e38a3687 5e              pop     rsi
00007ff9`e38a3688 5f              pop     rdi
00007ff9`e38a3689 c3              ret
00007ff9`e38a368a 8b5618          mov     edx,dword ptr [rsi+18h]
00007ff9`e38a368d ffc2            inc     edx
00007ff9`e38a368f 488bce          mov     rcx,rsi
00007ff9`e38a3692 90              nop
00007ff9`e38a3693 e8a8e60700      call    mscorlib_ni+0x621d40 (00007ff9`e3921d40) (System.Collections.Generic.List`1[[System.Int32, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5)
00007ff9`e38a3698 ebcd            jmp     mscorlib_ni+0x5a3667 (00007ff9`e38a3667)
00007ff9`e38a369a e8bf60f9ff      call    mscorlib_ni+0x53975e (00007ff9`e383975e) (mscorlib_ni)
00007ff9`e38a369f cc              int     3


0:000> !u 00007ff9e4428730
preJIT generated code
System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double)
Begin 00007ff9e4428730, size 5a
>>> 00007ff9`e4428730 56              push    rsi
00007ff9`e4428731 4883ec20        sub     rsp,20h
00007ff9`e4428735 488bf1          mov     rsi,rcx
00007ff9`e4428738 8b5618          mov     edx,dword ptr [rsi+18h]
00007ff9`e442873b 488b4e08        mov     rcx,qword ptr [rsi+8]
00007ff9`e442873f 3b5108          cmp     edx,dword ptr [rcx+8]
00007ff9`e4428742 7424            je      mscorlib_ni+0x1128768 (00007ff9`e4428768)
00007ff9`e4428744 488b5608        mov     rdx,qword ptr [rsi+8]
00007ff9`e4428748 8b4e18          mov     ecx,dword ptr [rsi+18h]
00007ff9`e442874b 8d4101          lea     eax,[rcx+1]
00007ff9`e442874e 894618          mov     dword ptr [rsi+18h],eax
00007ff9`e4428751 3b4a08          cmp     ecx,dword ptr [rdx+8]
00007ff9`e4428754 732e            jae     mscorlib_ni+0x1128784 (00007ff9`e4428784)
00007ff9`e4428756 4863c9          movsxd  rcx,ecx
00007ff9`e4428759 f20f114cca10    movsd   mmword ptr [rdx+rcx*8+10h],xmm1
00007ff9`e442875f ff461c          inc     dword ptr [rsi+1Ch]
00007ff9`e4428762 4883c420        add     rsp,20h
00007ff9`e4428766 5e              pop     rsi
00007ff9`e4428767 c3              ret
00007ff9`e4428768 f20f114c2438    movsd   mmword ptr [rsp+38h],xmm1
00007ff9`e442876e 8b5618          mov     edx,dword ptr [rsi+18h]
00007ff9`e4428771 ffc2            inc     edx
00007ff9`e4428773 488bce          mov     rcx,rsi
00007ff9`e4428776 90              nop
00007ff9`e4428777 e854fbffff      call    mscorlib_ni+0x11282d0 (00007ff9`e44282d0) (System.Collections.Generic.List`1[[System.Double, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5)
00007ff9`e442877c f20f104c2438    movsd   xmm1,mmword ptr [rsp+38h]
00007ff9`e4428782 ebc0            jmp     mscorlib_ni+0x1128744 (00007ff9`e4428744)
00007ff9`e4428784 e8d50f41ff      call    mscorlib_ni+0x53975e (00007ff9`e383975e) (mscorlib_ni)
00007ff9`e4428789 cc              int     3

/<code>

可能你有點蒙,我畫一張圖吧。

用了這麼多年的泛型,你對它到底有多瞭解?

四: 總結

泛型T真正的被代替是在 JIT編譯時才實現的,四個List 會生成四個具有相應具體類型的類對象,所以就不存在拆箱和裝箱的問題,而類型的限定visualstudio編譯器工具提前就幫我們約束好啦。


分享到:


相關文章: