用 Span 对 C# 进程中三大内存区域进行统一访问 ,太厉害了!
一:背景
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 -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 存在 数组中");
}
}
后面的两个问题就更加复杂了,既然 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
用 Span 对 C# 进程中三大内存区域进行统一访问 ,太厉害了!的更多相关文章
- windows进程中的内存结构[转载]
在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识. 接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据.那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变 ...
- windows进程中的内存结构(好多API,而且VC最聪明)
在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识. 接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据.那么这些变量在内存中是如何存放的呢?程序又是如何使用这 ...
- windows进程中的内存结构(缓冲溢出原理)
接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据.那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论.下文中的C语言代码如没有特别声明,默认都使 ...
- Java虚拟机中Java内存区域
Java虚拟机所管理的内存将会包括以下几个运行时数据区域. 程序计数器 可以看作是当前线程所执行的字节码的行号指示器. 每一个线程都需要有一个独立的程序计数器. 如果线程正在执行的是一个Java方 ...
- C++中的内存区域及其性能特征
首先须要指出的是.我们通经常使用"堆"和"自由存储"这两个术语来区分两种不同类型的动态分配内存. 1.常量数据:常量数据区域主要用于存储字符串以及其它在编译期就 ...
- .NET 4.0中使用内存映射文件实现进程通讯
操作系统很早就开始使用内存映射文件(Memory Mapped File)来作为进程间的共享存储区,这是一种非常高效的进程通讯手段.Win32 API中也包含有创建内存映射文件的函数,然而,这些函数都 ...
- Linux就这个范儿 第15章 七种武器 linux 同步IO: sync、fsync与fdatasync Linux中的内存大页面huge page/large page David Cutler Linux读写内存数据的三种方式
Linux就这个范儿 第15章 七种武器 linux 同步IO: sync.fsync与fdatasync Linux中的内存大页面huge page/large page David Cut ...
- Windows进程间共享内存通信实例
Windows进程间共享内存通信实例 抄抄补补整出来 采用内存映射文件实现WIN32进程间的通讯:Windows中的内存映射文件的机制为我们高效地操作文件提供了一种途径,它允许我们在WIN32进程中保 ...
- JVM内存区域划分(JDK6/7/8中的变化)
前言 Java程序的运行是通过Java虚拟机来实现的.通过类加载器将class字节码文件加载进JVM,然后根据预定的规则执行.Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同 ...
随机推荐
- session深入探讨
简介 session(会话),其实是一个容易让人误解的词.它总跟web系统的会话挂钩,利用session,javaweb项目实现了登录状态的控制.坊间流传,关闭浏览器,就是关闭了web系统的会话. 其 ...
- 解决flutter 运行时:Waiting for another flutter command to release the startup lock...
执行 Flutter 包管理相关命令时有可能遇到 Waiting for another flutter command to release the startup lock... 这样的错误,可尝 ...
- spark 笔记2
一.Spark Shuffle 的发展 Spark 0.8及以前 Hash Based Shuffle Spark 0.8.1 为Hash Based Shuffle引入File Consolidat ...
- APPCNA 指纹验证登录
今天在APP中集成了指纹与手势登录功能,本文章分两部分进行记录.一是手势功能的逻辑.二是代码实现.该APP是采用APPCAN开发,直接用其已写好的插件,调用相当接口就要可以了. 1.在APP的个人中心 ...
- Spring AOP系列(一)— 代理模式
Spring AOP系列(一)- 代理模式 AOP(Aspect Oriented Programming)并没有创造或使用新的技术,其底层就是基于代理模式实现.因此我们先来学习一下代理模式. 基本概 ...
- Python练习题 010:分解质因数
[Python练习题 010]将一个正整数分解质因数.例如:输入90,打印出90=2*3*3*5. -------------------------------------------------- ...
- Java 读取文件中的每一行,并为每一行插入特定的字符串
工具 1:Eclipse Java EE IDE for Web Developers. Version: Photon Release (4.8.0). Build id: 20180619-120 ...
- #ifndef, #define, #endif三者的作用
#ifndef, #define, #endif 作用 #ifndef 它是if not define 的简写,是宏定义的一种,实际上确切的说,这应该是预处理功能三种(宏定义.文件包含.条件编译) ...
- 在容器服务中获取客户端真实源 IP
适用范围:腾讯云容器服务(Tencent Kubernetes Engine ,TKE), 以下简称 TKE. 为什么需要获取客户端真实源 IP? 当需要能感知到服务请求来源去满足一些业务需求时,就需 ...
- 智能指针(1)-std::unique_ptr
std::unique_ptr std::unique_ptr是一种几乎和原始指针一样高效的智能指针,对所管理的指针资源拥有独占权.由C++11标准引入,用于替代C++98中过时的std::auto_ ...