C#利用一段字节序列构建一个数组对象
《.NET中的数组在内存中如何布局? 》介绍了一个.NET下针对数组对象的内存布局。既然我们知道了内存布局,我们自然可以按照这个布局规则创建一段字节序列来表示一个数组对象,就像《以纯二进制的形式在内存中绘制一个对象》构建一个普通的对象,以及《你知道.NET的字符串在内存中是如何存储的吗?》构建一个字符串对象一样。
一、数组类型布局
二、利用字节数组构建数组
三、利用非托管本地内存构建数组
四、性能测试
一、数组类型布局
我们再简单回顾一下数组对象的内存布局。如下图所示,对于32位(x86)系统,Object Header和TypeHandle各占据4个字节;但是对于64位(x64)来说,存储方法表指针的TypeHandle自然扩展到8个字节,但是Object Header依然是4个字节,为了确保TypeHandle基于8字节的内存对齐,所以会前置4个字节的“留白(Padding)”。
其荷载内容(Payload)采用如下的布局:前置4个字节以UInt32的形式存储数组的长度,后面依次存储每个数组元素的内容。对于64位(x64)来说,为了确保数组元素的内存对齐,两者之间具有4个字节的Padding。
二、利用字节数组构建数组
如下所示的BuildArray<T>方法帮助我们构建一个指定长度的数组,数组元素类型由泛型参数决定。如代码片段所示, 我们根据上述的内存布局规则计算出目标数组占据的字节数,并据此创建一个对应的字节数组来表示构建的数组。我们将数组类型(T[])的TypeHandle的值(方法表地址)写入对应的位置(偏移量和长度均为IntPtr.Size),紧随其后的4个字节写入数组的长度。自此一个指定元素类型/长度的空数组就已经构建出来了,我们让返回的数组变量指向数组的第IntPtr.Size个字节(4字节/8字节)。
unsafe static T[] BuildArray<T>(int length) { var byteCount = IntPtr.Size // Object header + Padding + IntPtr.Size // TypeHandle + IntPtr.Size // Length + Padding + Unsafe.SizeOf<T>() * length // Elements ; var bytes = new byte[byteCount]; Unsafe.Write(Unsafe.AsPointer(ref bytes[IntPtr.Size]), typeof(T[]).TypeHandle.Value); Unsafe.Write(Unsafe.AsPointer(ref bytes[IntPtr.Size * 2]), length); T[] array = null!; Unsafe.Write(Unsafe.AsPointer(ref array), new IntPtr(Unsafe.AsPointer(ref bytes[IntPtr.Size]))); return array; }
接下来我们就来验证一下BuildArray<T>构建的数组是否可以正常使用。如下面的代码片段所示,我们调用这个方法构建了一个长度位100的整型数组,并利用调试断言确定构建的数组长度是否正常,并验证每个元素是否置空。接下来我们对每个数组元素赋值,并利用调试断言验证赋值是否有效。
var array = BuildArray<int>(100); Debug.Assert(array.Length == 100); Debug.Assert(array.All(it => it == 0)); for (int index = 0; index < array.Length; index++) { array[index] = index; } for (int index = 0; index < array.Length; index++) { Debug.Assert(array[index] == index); }
上面演示的是值类型(Int32)数组的构建,下面采用类似的形式构建了一个引用类型(String)的数组。
var array = BuildArray<string>(100); Debug.Assert(array.Length == 100); Debug.Assert(array.All(it => it is null)); for (int index = 0; index < array.Length; index++) { array[index] = index.ToString(); } for (int index = 0; index < array.Length; index++) { Debug.Assert(array[index] == index.ToString()); }
三、利用非托管本地内存构建数组
既然我们可以利用一段连续的托管内存(字节数组)构建一个指定元素类型、指定长度的数组,我们自然也能利用非托管内存达到相同的目的。利用非托管本地内存构建数组带来的最大好处显而易见,那就是不会对GC造成任何压力,前提是我们能够自行释放分配的内容。为了我们将上面定义的BuildArray<T>方法改造成如下的形式:在完成针对字节数的计算之后,我们调用NativeMemory的AllocZeroed方法分配长度适合的内存,并将内容置空(设置为零)。接下来按照布局规则将TypeHandle和长度写入对应的位置。最后让返回的变量指向TypeHandle对应的地址就可以了。
unsafe static T[] BuildArray<T>(int length) { var byteCount = IntPtr.Size // Object header + Padding + IntPtr.Size // TypeHandle + IntPtr.Size // Length + Padding + Unsafe.SizeOf<T>() * length // Elements ; var pointer = NativeMemory.AllocZeroed((uint)byteCount); Unsafe.Write(Unsafe.Add<nint>(pointer, 1), typeof(T[]).TypeHandle.Value); Unsafe.Write(Unsafe.Add<nint>(pointer, 2), length); T[] array = null!; Unsafe.Write(Unsafe.AsPointer(ref array), new IntPtr(Unsafe.Add<nint>(pointer, 1))); return array; } unsafe static void Free<T>(T[] array) { var address = *(nint*)Unsafe.AsPointer(ref array); NativeMemory.Free(Unsafe.Add<nint>(address.ToPointer(), -1)); }
上面的代码还实现了用来释放本地内存的Free方法。我们通过对指定数组变量进行“解地址”得到带释放数组对象的地址,但是这个地址并非分配内存的初始位置,所有我们需要前移一个身位(InPtr.Size)得到指向初始内存地址的指针,并将其作为NativeMemory的Free方法的参数,这样在BuildArray<T>方法中分配的内存就能被释放了。
var random = new Random(); while (true) { var length = random.Next(10, 100); var array = BuildArray<int>(length); Debug.Assert(array.Length == length); Debug.Assert(array.All(it=>it == 0)); for (int index = 0; index < length; index++) array[index] = index; for (int index = 0; index<length; index++) Debug.Assert(array[index] == index); Free(array); }
在如下的演示程序中,我们在一个无限循环中调用BuildArray<T>方法构建一个随机长度的整型数组,然后我们利用调试断言验证其长度和元素初始值,然后对每个元素进行赋值并验证。由于每次循环都调用Free方法对创建的数组对象进行了释放,所以内存总是会维持在一个稳当的状态,这可以从VS提供的针对内存的诊断工具得到验证。
四、性能测试
我们最后做一个简单的性能测试看看BuildArray<T> + Free<T>与直接new T[]这两种编程方式的性能差异。如下面的代码片段所示,我们定义了两个Benchmark方法,ManagedArray方法直接返回利用new关键字创建的整型数组,长度为1024;NativeArray方法调用BuildArray<T>方法构建了一个相同长度的整型数组,并调用Free方法将其“释放”。
[MemoryDiagnoser] public class Benchmark { [Benchmark] public int[] ManagedArray()=> new int[1024]; [Benchmark] public void NativeArray()=>Free(BuildArray<int>(1024)); unsafe static T[] BuildArray<T>(int length); unsafe static void Free<T>(T[] array); }
如下所示的是性能测试的结果,可以看出NativeArray不仅仅没有基于GC的分配,耗时不到原来的一半。
相关文章:

【OpenCV】在Linux上使用OpenCvSharp
OpenCvSharp是一个OpenCV的 .Net wrapper,应用最新的OpenCV库开发,使用习惯比EmguCV更接近原始的OpenCV,该库采用LGPL发行,对商业应用友好。

在C#中调用C++函数并返回const char*类型的值
在C#中,使用DllImport特性将C++函数声明为外部函数。在Main方法中,调用generateProjectCode函数并将返回的指针转换为const char*类型的字符串。在C#中调用C++函数并返回const char*类型的值,可以使用Interop服务来实现。C++代码需要编译为动态链接库(DLL)。

C#winform上位机开发学习笔记3-串口助手的信息保存功能添加
上位机开发的系列学习笔记,避免遗忘多记录多补充多优化

C# 实现单线程异步互斥锁
C#对异步的支持越来越成熟,async、await简化了代码也提高了可读性,但由于在一段上下文中有了异步操作,意味着这段操作可能会被同时重复调用,如果本身没有被设计可以重复调用的情况下,就很可能会出问题。以上就是今天要讲的内容,本文简单的实现了单线程的异步互斥锁,实现起来相对简单,但作用还是比较大的。虽然说有些情况的异步是可以在前期设计上避免同时调用,比如登录按钮点击后出现蒙板不允许再次点击,但是对于已存在的代码出现了同时调用问题,此时有互斥锁则可以避免大范围改动代码,有效解决问题。_c#实现线程都要经过一个阻塞的方法,线程之间互不干涉

【小白专用】C# 连接 MySQL 数据库
C# 连接 MySQL 数据库

RTSP协议播放不兼容TPLINK摄像头的处理办法
报错的内容是Number of element invalid in origin string.两个数字中间多了一个空格,导致判断数据不等于6。所以数据输入的时候把中间的空格去掉一个即可。

C#实现Excel合并单元格数据导入数据集
C#实现Excel合并单元格数据导入数据集

即将消失的五种编程语言?
学习路径困难必然导致非常有限的活跃用户,而 Haskell 的上一个最新的稳定版本是在 2010 年发布,这对于促进它本身的发展无济于事。Perl 于 1987 年开始流行时,它被誉为是适合任何一个人的编程语言,曾经有一段时间,每个人都用Perl编程,但是后来发生了一些事情,开发者开始在不知道原因的情况下添加越来越大的功能,也许这增加了了问题的复杂性。甚至它的作者似乎已经含蓄地解释了Perl的一些问题,并选择停止从2000年开始的Perl 6开发,关键是,似乎现在也没人想要在用Perl。

c#调试程序一次启动两个工程(多个工程)
可以在解决方案中设置多个启动项目(右键单击解决方案,转到设置启动项目,选择多个启动项目),并为包含在解决方案(无开始不调试就开始如果您将多个项目设置为开始,则调试器将在启动时附加到每个项目。

Leetcode算法系列| 11. 盛最多水的容器
给定一个长度为 n 的整数数组 height。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。说明:你不能倾斜容器。

C#使用 OpenHardwareMonitor获取CPU或显卡温度、使用率、时钟频率相关方式
代码的功能可以将主板的名称显示出来,还有将第一个CPU的情况显示,可以根据实际情况进行修改。C# 去获取电脑相关的基础信息,还是需要借助 外部的库,我这边尝试了自己去实现它。OpenHardwareMonitor获取CPU的温度和频率需要管理员权限。网上有一些信息,但不太完整,都比较零碎,这边尽量将代码完整的去展示出来。引用–>添加引用—>浏览(选择文件)–>确定。代码中注释掉的部分是循环显示的一个循环逻辑。在没有开权限的时候就是无法使用。

C# 读取Word表格到DataSet
在应用项目里,多数情况下我们会遇到导入 Excel 文件数据到数据库的功能需求,但某些情况下,也存在使用 Word 进行表格数据编辑的情况。Word 和 Excel 其实各有特点,但用户的习惯不同,即使同一数据源,可能提供的数据源文件类型也不同,这其中也包括导入Word内容的功能,比如表格数据导出到DataSet数据集。

C#中var、object和dynamic的区别
在C#中,var、object和dynamic这三个关键字具有不同的特性和用途。var关键字用于隐式类型推断,编译时确定变量类型,一旦确定后不能更改。object关键字是C#中的基础类型,可以保存任意类型的数据,但需要进行装箱和拆箱操作,并且需要显式类型转换才能获取原始数据。dynamic关键字用于动态类型,变量类型可以在运行时推断并更改,避免了显式类型转换的繁琐,但会带来一些性能开销和运行时错误的风险。根据具体的需求和场景,选择合适的关键字进行变量声明和操作是编写高效和可读性良好代码的关键。

文档管理系统的核心技术与难点
概述网上有非常多的“文档管理系统”,随便搜索就能得到超过1000种大大小小的软件或系统,谓之“铺天盖地”也不为过。其中绝大多数是近几年用各类开源的所谓组件、框架搭起来的七拼八凑的产物,其花哨无比的言辞与看似不错的截图,会造成很多用户茫然,掏钱购买后基本上都感觉交了智商税。那么到底什么样的系统才能称为“文档管理系统”呢?怎么选择比较安全呢?先回答第二个问题:世界上任何一个能用的软件至少需要5年的基本成长期。所以,选购的时候,5年以内的软件,就不要考虑了。后面是几个基本概念。文档管理也是各类信息系统

一款跨空间、跨平台、能分享、能搜索常用文件内容、能识别图片文字的全能搜索工具
多可文件快搜安装简单,无需复杂配置。安装在本机后,不仅能搜索本机文件,还可以搜索局域网内共享文件。它可以搜索NAS(SMB协议)上的文件。就连存储在阿里云OSS里的文件,也能轻松搜索到。它还支持IPv6,使用户可以快速安全地搜索网络中的文件。

C/C++,FEISTDLIB的部分源代码
C/C++,FEISTDLIB的部分源代码

C/C++,动态 DP 问题的计算方法与源程序
C/C++,动态 DP 问题的计算方法与源程序

C/C++,图算法——Dinic最大流量算法
C/C++,图算法——Dinic最大流量算法

C#winform根据选择的Excel文件在数据库中创建数据表
C#winform根据选择的Excel文件在数据库中创建数据表

C/C++,组合算法——K人活动选择问题(Activity-Selection-Problem)的源程序
C/C++,组合算法——K人活动选择问题(Activity-Selection-Problem)的源程序

Asp.Net Core Web Api内存泄漏问题
使用Asp.Net Core Web Api框架开发网站中使用到了tcp socket通信,网站作为服务端开始tcp server,其他的客户端不断高速给它传输信息时,tcp server中读取信息每次申请的byte[]没有得到及时的释放,导致内存浪费越来越多,最终内存溢出,系统崩溃。而使用Asp.Net Core Web Api框架搭建的项目中跑这个服务端代码,则是这样的,很少引发GC,没有及时回收buffer数组的无效内存空间。,则正常引发GC,每次申请的buffer数组都得到及时的释放。

C# 实现微信退款及对帐
文章浏览阅读1.6k次,点赞77次,收藏84次。本次我们以微信支付进行举例,在考生注册账号、编写简历、报名职位、被初审核通过等一系列基础的条件的具备下,可以进入支付考务费的环节(笔试费用),我们会为其生成一个支付二维码,考生支付后(无论成功与否),都会记录其支付结果状态。以上提供的代码仅供参考,在实际的应用中,我们还可以根据业务需要编写其它功能,如下载微信官方对帐单,导入到应用系统中,与业务数据进行对帐,以排查争议数据;退款申请成功后,仅为申请状态,需要通过查询退款情况以确定是否完成,该功能可以在考生方进行实现,考生可随时查询自己的对帐情况。

C#,数值计算——分类与推理Svmlinkernel的计算方法与源程序
文章浏览阅读27次。C#,数值计算——分类与推理Svmlinkernel的计算方法与源程序