一:背景

1. 讲故事

前段时间写了几篇 C# 漫文,评论留言中有很多敌人屡次提到 Span,周末抽空看了下,的确是一个十分????????的新构造,让我想到了当年的WCF,它对立了.NET下各种零散的分布式技术,包含:.NET Remoteing,WebService,NamedPipe,MSMQ,而这里的 Span 对立了 C# 过程中的三大块内存拜访,包含:栈内存, 托管堆内存, 非托管堆内存,画个图如下:

接下来就和大家具体聊聊这三大块的内存对立拜访。

二: 过程中的三大块内存解析

1. 栈内存

大家应该晓得办法内的局部变量是寄存在栈上的,而且每一个线程默认会被调配 1M 的内存空间,我举个例子:

        static void Main(string[] args)        {            int i = 10;            long j = 20;            List<string> list = new List<string>();        }

下面 i,j 的值都是存于栈上,list的堆上内存地址也是存于栈上,为了看个到底,能够用 windbg 验证一下:

0:000> !clrstack -lOS Thread Id: 0x2708 (0)        Child SP               IP Call Site00000072E47CE558 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堆地址,除了这些简略类型,还能够在栈上调配简单类型,这里就要用到 stackalloc 关键词, 如下代码:

 int* ptr = stackalloc int[3] { 10, 11, 12 };

问题就在这里,指针类型尽管灵便,然而做任何事件都比拟繁琐,比如说:

  • 查找某一个数是否在 int[] 中
  • 反转 int[]
  • 剔除尾部的某一个数字(比方 12)

就拿第一个问题来说,操作指针的代码如下:

            //指针接管            int* ptr = stackalloc int[3] { 10, 11, 12 };            //蕴含判断            for (int i = 0; i < 3; i++)            {                if (*ptr++ == 11)                {                    Console.WriteLine(" 11 存在 数组中");                }            }

前面的两个问题就更加简单了,既然 Span 是对立拜访,就应该用 Span 来接 stackalloc,代码如下:

            Span<int> span = stackalloc int[3] { 10, 11, 12 };            //1. 是否蕴含            var hasNum = span.Contains(11);            //2. 反转            span.Reverse();            //3. 剔除尾部            span.Trim(12);

这就很????????了,你既不须要接触指针,又能实现指针的大部分操作,而且还特地便捷,拜服,最初来验证一下 int[] 是否真的在 线程栈 上。

0:000> !clrstack -l000000ED7737E4B0 00007FF7C4EA16AD DataStruct.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 28]    LOCALS:        0x000000ED7737E570 = 0x000000ed7737e4d0        0x000000ED7737E56C = 0x0000000000000001        0x000000ED7737E558 = 0x000000ed7737e4d00:000> dp 0x000000ed7737e4d0000000ed`7737e4d0  0000000b`0000000c 00000000`0000000a

从 Locals 处的 0x000000ED7737E570 = 0x000000ed7737e4d0 能够看到 key / value 是十分相近的,阐明在栈上无疑。

从最初一行 a,b,c 可看出对应的就是数组中的 10,11,12。

2. 非托管堆内存

说到非托管内存,让我想起了当年 C# 调用 C++ 的场景,代码到处充斥着相似上面的语句:

        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) 就用到了非托管内存,从当初开始你就能够用 Span 来接 Marshal.AllocHGlobal 调配的非托管内存啦!????????????,如下代码所示:

    class Program    {        static unsafe void Main(string[] args)        {            var ptr = Marshal.AllocHGlobal(3);            //将 ptr 转换为 span            var span = new Span<byte>((byte*)ptr, 3) { [0] = 10, [1] = 11, [2] = 12 };            //而后在  span 中能够进行各种操作了。。。            Marshal.FreeHGlobal(ptr);        }    }

这里我也用 windbg 给大家看一下 未托管内存 在内存中是个什么样子。

0:000> !clrstack -lOS Thread Id: 0x3b10 (0)        Child SP               IP Call Site000000A51777E758 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 = 0x00000274901447600:000> dp 0x000002749014476000000274`90144760  abababab`ab0c0b0a abababab`abababab        

最初一行的 0c0b0a 这就是低位到高位的 10,11,12 三个数,接下来从 Locals 处 0x000000A51777EA58 = 0x0000027490144760 能够看出,这个key,value 相隔十万八千里,阐明必定不在栈内存中,持续用 windbg 甄别一下 0x0000027490144760 是否是托管堆上,能够用 !eeheap -gc 查看托管堆地址范畴,如下代码:

0:000> !eeheap -gcNumber of GC Heaps: 1generation 0 starts at 0x00000274901B1030generation 1 starts at 0x00000274901B1018generation 2 starts at 0x00000274901B1000ephemeral segment allocation context: none         segment             begin         allocated              size00000274901B0000  00000274901B1000  00000274901C5370  0x14370(82800)Large object heap starts at 0x00000274A01B1000         segment             begin         allocated              size00000274A01B0000  00000274A01B1000  00000274A01B5480  0x4480(17536)Total Size:              Size: 0x187f0 (100336) bytes.------------------------------GC Heap Size:    Size: 0x187f0 (100336) bytes.

从下面信息能够看到,0x0000027490144760 显著不在:3代堆:00000274901B1000 ~ 00000274901C5370 和 大对象堆:00000274A01B1000 ~ 00000274A01B5480 区间范畴内。

3. 托管堆内存

用 Span 对立托管内存拜访那是相当简略了,如下代码所示:

   Span<byte> span = new byte[3] { 10, 11, 12 };

同样,你有了Span,你就能够应用 Span 自带的各种办法,这里就不多介绍了,大家有趣味能够实操一下。

三: 总结

总的来说,这一篇次要是从思维上带大家一起意识 Span,以及如何用 Span 对接 三大区域内存,对于 Span 的益处以及源码解析,前面上专门的文章吧!

更多高质量干货:参见我的 GitHub: dotnetfly