6

用 Span 对 C# 进程中三大内存区域进行统一访问 ,太厉害了!

 3 years ago
source link: https://www.cnblogs.com/huangxincheng/p/13876859.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. 讲故事

前段时间写了几篇 C# 漫文,评论留言中有很多朋友多次提到 Span,周末抽空看了下,确实是一个非常🐂👃的新结构,让我想到了当年的WCF,它统一了.NET下各种零散的分布式技术,包括:.NET Remoteing,WebService,NamedPipe,MSMQ,而这里的 Span 统一了 C# 进程中的三大块内存访问,包括:栈内存, 托管堆内存, 非托管堆内存,画个图如下:

214741-20201026093942510-1710172149.png

接下来就和大家具体聊聊这三大块的内存统一访问。

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

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 -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堆地址,除了这些简单类型,还可以在栈上分配复杂类型,这里就要用到 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 存在 数组中");
                }
            }

214741-20201026093943542-261305890.png

后面的两个问题就更加复杂了,既然 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 -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 可看出对应的就是数组中的 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 -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 三个数,接下来从 Locals 处 0x000000A51777EA58 = 0x0000027490144760 可以看出,这个key,value 相隔十万八千里,说明肯定不在栈内存中,继续用 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 和 大对象堆:00000274A01B1000 ~ 00000274A01B5480 区间范围内。

3. 托管堆内存

用 Span 统一托管内存访问那是相当简单了,如下代码所示:


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

同样,你有了Span,你就可以使用 Span 自带的各种方法,这里就不多介绍了,大家有兴趣可以实操一下。

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

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

图片名称

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK