用 Span 對(duì) C# 進(jìn)程中三大內(nèi)存區(qū)域進(jìn)行統(tǒng)一訪問 ,太厲害了!
一:背景
1. 講故事
前段時(shí)間寫了幾篇 C# 漫文,評(píng)論留言中有很多朋友多次提到 Span,周末抽空看了下,確實(shí)是一個(gè)非常的新結(jié)構(gòu),讓我想到了當(dāng)年的WCF,它統(tǒng)一了.NET下各種零散的分布式技術(shù),包括:.NET Remoteing,WebService,NamedPipe,MSMQ,而這里的 Span 統(tǒng)一了 C# 進(jìn)程中的三大塊內(nèi)存訪問,包括:棧內(nèi)存, 托管堆內(nèi)存, 非托管堆內(nèi)存,畫個(gè)圖如下:
接下來就和大家具體聊聊這三大塊的內(nèi)存統(tǒng)一訪問。
二:進(jìn)程中的三大塊內(nèi)存解析
1. 棧內(nèi)存
大家應(yīng)該知道方法內(nèi)的局部變量是存放在棧上的,而且每一個(gè)線程默認(rèn)會(huì)被分配 1M 的內(nèi)存空間,我舉個(gè)例子:
- static void Main(string[] args)
- {
- int i = 10;
- long j = 20;
- List<string> list = new List<string>();
- }
上面 i,j 的值都是存于棧上,list的堆上內(nèi)存地址也是存于棧上,為了看個(gè)究竟,可以用 windbg 驗(yàn)證一下:
- 0:000> !clrstack -l
- OS Thread Id: 0x2708 (0)
- Child SP IP Call Site
- 00000072E47CE558 00007ff89cf7c184 [InlinedCallFrame: 00000072e47ce558] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
- 00000072E47CE558 00007ff7c7c03fd8 [InlinedCallFrame: 00000072e47ce558] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
- 00000072E47CE520 00007FF7C7C03FD8 ILStubClass.IL_STUB_PInvoke(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
- 00000072E47CE7B0 00007FF8541E530D System.Console.ReadLine()
- 00000072E47CE7E0 00007FF7C7C0101E DataStruct.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 22]
- LOCALS:
- 0x00000072E47CE82C = 0x000000000000000a
- 0x00000072E47CE820 = 0x0000000000000014
- 0x00000072E47CE818 = 0x0000018015aeab10
通過 clrstack -l 查看線程棧,最后三行可以明顯的看到 0a -> 10, 14 -> 20 , 0xxxxxxb10 => list堆地址,除了這些簡(jiǎn)單類型,還可以在棧上分配復(fù)雜類型,這里就要用到 stackalloc 關(guān)鍵詞, 如下代碼:
- int* ptr = stackalloc int[3] { 10, 11, 12 };
問題就在這里,指針類型雖然靈活,但是做任何事情都比較繁瑣,比如說:
- 查找某一個(gè)數(shù)是否在 int[] 中
- 反轉(zhuǎn) int[]
- 剔除尾部的某一個(gè)數(shù)字(比如 12)
就拿第一個(gè)問題來說,操作指針的代碼如下:
- //指針接收
- int* ptr = stackalloc int[3] { 10, 11, 12 };
- //包含判斷
- for (int i = 0; i < 3; i++)
- {
- if (*ptr++ == 11)
- {
- Console.WriteLine(" 11 存在 數(shù)組中");
- }
- }
后面的兩個(gè)問題就更加復(fù)雜了,既然 Span 是統(tǒng)一訪問,就應(yīng)該用 Span 來接 stackalloc,代碼如下:
- Span<int> span = stackalloc int[3] { 10, 11, 12 };
- //1. 是否包含
- var hasNum = span.Contains(11);
- //2. 反轉(zhuǎn)
- span.Reverse();
- //3. 剔除尾部
- span.Trim(12);
這就很了,你既不需要接觸指針,又能完成指針的大部分操作,而且還特別便捷,佩服,最后來驗(yàn)證一下 int[] 是否真的在 線程棧 上。
- 0:000> !clrstack -l
- 000000ED7737E4B0 00007FF7C4EA16AD DataStruct.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 28]
- LOCALS:
- 0x000000ED7737E570 = 0x000000ed7737e4d0
- 0x000000ED7737E56C = 0x0000000000000001
- 0x000000ED7737E558 = 0x000000ed7737e4d0
- 0:000> dp 0x000000ed7737e4d0
- 000000ed`7737e4d0 0000000b`0000000c 00000000`0000000a
從 Locals 處的 0x000000ED7737E570 = 0x000000ed7737e4d0 可以看到 key / value 是非常相近的,說明在棧上無疑。
從最后一行 a,b,c 可看出對(duì)應(yīng)的就是數(shù)組中的 10,11,12。
2. 非托管堆內(nèi)存
說到非托管內(nèi)存,讓我想起了當(dāng)年 C# 調(diào)用 C++ 的場(chǎng)景,代碼到處充斥著類似下面的語句:
- private bool SendMessage(int messageType, string ip, string port, int length, byte[] messageBytes)
- {
- bool result = false;
- if (windowHandle != 0)
- {
- var bytes = new byte[Const.MaxLengthOfBuffer];
- Array.Copy(messageBytes, bytes, messageBytes.Length);
- int sizeOfType = Marshal.SizeOf(typeof(StClientData));
- StClientData stData = new StClientData
- {
- Ip = GlobalConvert.IpAddressToUInt32(IPAddress.Parse(ip)),
- Port = Convert.ToInt16(port),
- Length = Convert.ToUInt32(length),
- Buffer = bytes
- };
- int sizeOfStData = Marshal.SizeOf(stData);
- IntPtr pointer = Marshal.AllocHGlobal(sizeOfStData);
- Marshal.StructureToPtr(stData, pointer, true);
- CopyData copyData = new CopyData
- {
- DwData = (IntPtr)messageType,
- CbData = Marshal.SizeOf(sizeOfType),
- LpData = pointer
- };
- SendMessage(windowHandle, WmCopydata, 0, ref copyData);
- Marshal.FreeHGlobal(pointer);
- string data = GlobalConvert.ByteArrayToHexString(messageBytes);
- CommunicationManager.Instance.SendDebugInfo(new DataSendEventArgs() { Data = data });
- result = true;
- }
- return result;
- }
上面代碼中的: IntPtr pointer = Marshal.AllocHGlobal(sizeOfStData); 和 Marshal.FreeHGlobal(pointer) 就用到了非托管內(nèi)存,從現(xiàn)在開始你就可以用 Span 來接 Marshal.AllocHGlobal 分配的非托管內(nèi)存啦!,如下代碼所示:
- class Program
- {
- static unsafe void Main(string[] args)
- {
- var ptr = Marshal.AllocHGlobal(3);
- //將 ptr 轉(zhuǎn)換為 span
- var span = new Span<byte>((byte*)ptr, 3) { [0] = 10, [1] = 11, [2] = 12 };
- //然后在 span 中可以進(jìn)行各種操作了。。。
- Marshal.FreeHGlobal(ptr);
- }
- }
這里我也用 windbg 給大家看一下 未托管內(nèi)存 在內(nèi)存中是個(gè)什么樣子。
- 0:000> !clrstack -l
- OS Thread Id: 0x3b10 (0)
- Child SP IP Call Site
- 000000A51777E758 00007ff89cf7c184 [InlinedCallFrame: 000000a51777e758] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
- 000000A51777E758 00007ff7c4654dd8 [InlinedCallFrame: 000000a51777e758] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
- 000000A51777E720 00007FF7C4654DD8 ILStubClass.IL_STUB_PInvoke(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
- 000000A51777E9E0 00007FF7C46511D0 DataStruct.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 26]
- LOCALS:
- 0x000000A51777EA58 = 0x0000027490144760
- 0x000000A51777EA48 = 0x0000027490144760
- 0x000000A51777EA38 = 0x0000027490144760
- 0:000> dp 0x0000027490144760
- 00000274`90144760 abababab`ab0c0b0a abababab`abababab
最后一行的 0c0b0a 這就是低位到高位的 10,11,12 三個(gè)數(shù),接下來從 Locals 處 0x000000A51777EA58 = 0x0000027490144760 可以看出,這個(gè)key,value 相隔十萬八千里,說明肯定不在棧內(nèi)存中,繼續(xù)用 windbg 鑒別一下 0x0000027490144760 是否是托管堆上,可以用 !eeheap -gc 查看托管堆地址范圍,如下代碼:
- 0:000> !eeheap -gc
- Number of GC Heaps: 1
- generation 0 starts at 0x00000274901B1030
- generation 1 starts at 0x00000274901B1018
- generation 2 starts at 0x00000274901B1000
- ephemeral segment allocation context: none
- segment begin allocated size
- 00000274901B0000 00000274901B1000 00000274901C5370 0x14370(82800)
- Large object heap starts at 0x00000274A01B1000
- segment begin allocated size
- 00000274A01B0000 00000274A01B1000 00000274A01B5480 0x4480(17536)
- Total Size: Size: 0x187f0 (100336) bytes.
- ------------------------------
- GC Heap Size: Size: 0x187f0 (100336) bytes.
從上面信息可以看到,0x0000027490144760 明顯不在:3代堆:00000274901B1000 ~ 00000274901C5370 和 大對(duì)象堆:00000274A01B1000 ~ 00000274A01B5480 區(qū)間范圍內(nèi)。
3. 托管堆內(nèi)存
用 Span 統(tǒng)一托管內(nèi)存訪問那是相當(dāng)簡(jiǎn)單了,如下代碼所示:
Span
同樣,你有了Span,你就可以使用 Span 自帶的各種方法,這里就不多介紹了,大家有興趣可以實(shí)操一下。
三:總結(jié)
總的來說,這一篇主要是從思想上帶大家一起認(rèn)識(shí) Span,以及如何用 Span 對(duì)接 三大區(qū)域內(nèi)存,關(guān)于 Span 的好處以及源碼解析,后面上專門的文章吧!
本文轉(zhuǎn)載自微信公眾號(hào)「 一線碼農(nóng)聊技術(shù)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 一線碼農(nóng)聊技術(shù)公眾號(hào)。