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

 一:背景

十年的市北网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。全网营销推广的优势是能够根据用户设备显示端的尺寸不同,自动调整市北建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。成都创新互联从事“市北网站设计”,“市北网站推广”以来,每个客户项目都认真落实执行。

1. 讲故事

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

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

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

1. 栈内存

大家应该知道方法内的局部变量是存放在栈上的,而且每一个线程默认会被分配 1M 的内存空间,我举个例子:

 
 
 
 
  1. static void Main(string[] args)
  2.         {
  3.             int i = 10;
  4.             long j = 20;
  5.             List list = new List();
  6.         }

上面 i,j 的值都是存于栈上,list的堆上内存地址也是存于栈上,为了看个究竟,可以用 windbg 验证一下:

 
 
 
 
  1. 0:000> !clrstack -l
  2. OS Thread Id: 0x2708 (0)
  3.         Child SP               IP Call Site
  4. 00000072E47CE558 00007ff89cf7c184 [InlinedCallFrame: 00000072e47ce558] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
  5. 00000072E47CE558 00007ff7c7c03fd8 [InlinedCallFrame: 00000072e47ce558] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
  6. 00000072E47CE520 00007FF7C7C03FD8 ILStubClass.IL_STUB_PInvoke(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
  7. 00000072E47CE7B0 00007FF8541E530D System.Console.ReadLine()
  8. 00000072E47CE7E0 00007FF7C7C0101E DataStruct.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 22]
  9.     LOCALS:
  10.         0x00000072E47CE82C = 0x000000000000000a
  11.         0x00000072E47CE820 = 0x0000000000000014
  12.         0x00000072E47CE818 = 0x0000018015aeab10

通过 clrstack -l 查看线程栈,最后三行可以明显的看到 0a -> 10, 14 -> 20 , 0xxxxxxb10 => list堆地址,除了这些简单类型,还可以在栈上分配复杂类型,这里就要用到 stackalloc 关键词, 如下代码:

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

问题就在这里,指针类型虽然灵活,但是做任何事情都比较繁琐,比如说:

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

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

 
 
 
 
  1. //指针接收
  2.             int* ptr = stackalloc int[3] { 10, 11, 12 };
  3.             //包含判断
  4.             for (int i = 0; i < 3; i++)
  5.             {
  6.                 if (*ptr++ == 11)
  7.                 {
  8.                     Console.WriteLine(" 11 存在 数组中");
  9.                 }
  10.             }

后面的两个问题就更加复杂了,既然 Span 是统一访问,就应该用 Span 来接 stackalloc,代码如下:

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

这就很了,你既不需要接触指针,又能完成指针的大部分操作,而且还特别便捷,佩服,最后来验证一下 int[] 是否真的在 线程栈 上。

 
 
 
 
  1. 0:000> !clrstack -l
  2. 000000ED7737E4B0 00007FF7C4EA16AD DataStruct.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 28]
  3.     LOCALS:
  4.         0x000000ED7737E570 = 0x000000ed7737e4d0
  5.         0x000000ED7737E56C = 0x0000000000000001
  6.         0x000000ED7737E558 = 0x000000ed7737e4d0
  7. 0:000> dp 0x000000ed7737e4d0
  8. 000000ed`7737e4d0  0000000b`0000000c 00000000`0000000a

从 Locals 处的 0x000000ED7737E570 = 0x000000ed7737e4d0 可以看到 key / value 是非常相近的,说明在栈上无疑。

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

2. 非托管堆内存

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

 
 
 
 
  1. private bool SendMessage(int messageType, string ip, string port, int length, byte[] messageBytes)
  2.         {
  3.             bool result = false;
  4.             if (windowHandle != 0)
  5.             {
  6.                 var bytes = new byte[Const.MaxLengthOfBuffer];
  7.                 Array.Copy(messageBytes, bytes, messageBytes.Length);
  8.                 int sizeOfType = Marshal.SizeOf(typeof(StClientData));
  9.                 StClientData stData = new StClientData
  10.                 {
  11.                     Ip = GlobalConvert.IpAddressToUInt32(IPAddress.Parse(ip)),
  12.                     Port = Convert.ToInt16(port),
  13.                     Length = Convert.ToUInt32(length),
  14.                     Buffer = bytes
  15.                 };
  16.                 int sizeOfStData = Marshal.SizeOf(stData);
  17.                 IntPtr pointer = Marshal.AllocHGlobal(sizeOfStData);
  18.                 Marshal.StructureToPtr(stData, pointer, true);
  19.                 CopyData copyData = new CopyData
  20.                 {
  21.                     DwData = (IntPtr)messageType,
  22.                     CbData = Marshal.SizeOf(sizeOfType),
  23.                     LpData = pointer
  24.                 };
  25.                 SendMessage(windowHandle, WmCopydata, 0, ref copyData);
  26.                 Marshal.FreeHGlobal(pointer);
  27.                 string data = GlobalConvert.ByteArrayToHexString(messageBytes);
  28.                 CommunicationManager.Instance.SendDebugInfo(new DataSendEventArgs() { Data = data });
  29.                 result = true;
  30.             }
  31.             return result;
  32.         }

上面代码中的: IntPtr pointer = Marshal.AllocHGlobal(sizeOfStData); 和 Marshal.FreeHGlobal(pointer) 就用到了非托管内存,从现在开始你就可以用 Span 来接 Marshal.AllocHGlobal 分配的非托管内存啦!,如下代码所示:

 
 
 
 
  1. class Program
  2.     {
  3.         static unsafe void Main(string[] args)
  4.         {
  5.             var ptr = Marshal.AllocHGlobal(3);
  6.             //将 ptr 转换为 span
  7.             var span = new Span((byte*)ptr, 3) { [0] = 10, [1] = 11, [2] = 12 };
  8.             //然后在  span 中可以进行各种操作了。。。
  9.             Marshal.FreeHGlobal(ptr);
  10.         }
  11.     }

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

 
 
 
 
  1. 0:000> !clrstack -l
  2. OS Thread Id: 0x3b10 (0)
  3.         Child SP               IP Call Site
  4. 000000A51777E758 00007ff89cf7c184 [InlinedCallFrame: 000000a51777e758] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
  5. 000000A51777E758 00007ff7c4654dd8 [InlinedCallFrame: 000000a51777e758] Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
  6. 000000A51777E720 00007FF7C4654DD8 ILStubClass.IL_STUB_PInvoke(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
  7. 000000A51777E9E0 00007FF7C46511D0 DataStruct.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 26]
  8.     LOCALS:
  9.         0x000000A51777EA58 = 0x0000027490144760
  10.         0x000000A51777EA48 = 0x0000027490144760
  11.         0x000000A51777EA38 = 0x0000027490144760
  12. 0:000> dp 0x0000027490144760
  13. 00000274`90144760  abababab`ab0c0b0a abababab`abababab        

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

 
 
 
 
  1. 0:000> !eeheap -gc
  2. Number of GC Heaps: 1
  3. generation 0 starts at 0x00000274901B1030
  4. generation 1 starts at 0x00000274901B1018
  5. generation 2 starts at 0x00000274901B1000
  6. ephemeral segment allocation context: none
  7.          segment             begin         allocated              size
  8. 00000274901B0000  00000274901B1000  00000274901C5370  0x14370(82800)
  9. Large object heap starts at 0x00000274A01B1000
  10.          segment             begin         allocated              size
  11. 00000274A01B0000  00000274A01B1000  00000274A01B5480  0x4480(17536)
  12. Total Size:              Size: 0x187f0 (100336) bytes.
  13. ------------------------------
  14. GC Heap Size:    Size: 0x187f0 (100336) bytes.

从上面信息可以看到,0x0000027490144760 明显不在:3代堆:00000274901B1000 ~ 00000274901C5370 和 大对象堆:00000274A01B1000 ~ 00000274A01B5480 区间范围内。

3. 托管堆内存

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

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

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

三:总结

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

本文转载自微信公众号「 一线码农聊技术」,可以通过以下二维码关注。转载本文请联系 一线码农聊技术公众号。

当前标题:用Span对C#进程中三大内存区域进行统一访问,太厉害了!
网址分享:http://www.36103.cn/qtweb/news3/36553.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联