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

存储 存储软件 后端
总的来说,这一篇主要是从思想上带大家一起认识 Span,以及如何用 Span 对接 三大区域内存,关于 Span 的好处以及源码解析。

[[348727]]

 一:背景

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<string> list = new List<string>(); 
  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.             //包含判断 
  5.             for (int i = 0; i < 3; i++) 
  6.             { 
  7.                 if (*ptr++ == 11) 
  8.                 { 
  9.                     Console.WriteLine(" 11 存在 数组中"); 
  10.                 } 
  11.             } 

 

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

  1. Span<int> span = stackalloc int[3] { 10, 11, 12 }; 
  2.  
  3.             //1. 是否包含 
  4.             var hasNum = span.Contains(11); 
  5.  
  6.             //2. 反转 
  7.             span.Reverse(); 
  8.  
  9.             //3. 剔除尾部 
  10.             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.  
  8. 0:000> dp 0x000000ed7737e4d0 
  9. 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.  
  9.                 int sizeOfType = Marshal.SizeOf(typeof(StClientData)); 
  10.  
  11.                 StClientData stData = new StClientData 
  12.                 { 
  13.                     Ip = GlobalConvert.IpAddressToUInt32(IPAddress.Parse(ip)), 
  14.                     Port = Convert.ToInt16(port), 
  15.                     Length = Convert.ToUInt32(length), 
  16.                     Buffer = bytes 
  17.                 }; 
  18.  
  19.  
  20.                 int sizeOfStData = Marshal.SizeOf(stData); 
  21.  
  22.                 IntPtr pointer = Marshal.AllocHGlobal(sizeOfStData); 
  23.  
  24.                 Marshal.StructureToPtr(stData, pointer, true); 
  25.  
  26.                 CopyData copyData = new CopyData 
  27.                 { 
  28.                     DwData = (IntPtr)messageType, 
  29.                     CbData = Marshal.SizeOf(sizeOfType), 
  30.                     LpData = pointer 
  31.                 }; 
  32.  
  33.                 SendMessage(windowHandle, WmCopydata, 0, ref copyData); 
  34.  
  35.                 Marshal.FreeHGlobal(pointer); 
  36.  
  37.                 string data = GlobalConvert.ByteArrayToHexString(messageBytes); 
  38.                 CommunicationManager.Instance.SendDebugInfo(new DataSendEventArgs() { Data = data }); 
  39.  
  40.                 result = true
  41.             } 
  42.             return result; 
  43.         } 

上面代码中的: 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.  
  7.             //将 ptr 转换为 span 
  8.             var span = new Span<byte>((byte*)ptr, 3) { [0] = 10, [1] = 11, [2] = 12 }; 
  9.  
  10.             //然后在  span 中可以进行各种操作了。。。 
  11.  
  12.             Marshal.FreeHGlobal(ptr); 
  13.         } 
  14.     } 

这里我也用 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.  
  13. 0:000> dp 0x0000027490144760 
  14. 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 的好处以及源码解析,后面上专门的文章吧!

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

 

 

责任编辑:武晓燕 来源: 一线码农聊技术
相关推荐

2021-11-01 07:50:44

TomcatWeb应用

2022-06-06 07:52:00

Python大风车

2021-10-08 13:38:23

手机系统鸿蒙

2022-04-08 08:11:28

Python代码

2023-03-03 09:11:55

软件开发NASA

2017-02-23 08:00:04

智能语音Click

2018-04-11 14:30:33

2011-04-13 16:50:54

CC++内存

2011-06-21 11:16:24

cc++

2023-09-08 09:12:57

内存缓存图像

2018-05-14 22:58:14

戴尔

2021-03-01 12:06:12

Nginx命令Linux

2023-05-06 06:47:46

Bing聊天机器人

2023-11-01 08:07:42

.NETC#

2019-02-12 11:07:49

2024-02-26 12:42:40

2024-02-26 13:47:00

C#Socket数据接收

2009-08-31 14:45:10

C#扩展方法

2018-03-09 09:42:51

2021-12-27 07:59:50

ECMAScript JSON模块Node.js
点赞
收藏

51CTO技术栈公众号