指针和函数指针指针相信大家都不陌生,像 C/C++ 中的指针那样,C# 中套一个 unsafe
就能直接用 。唯一需要注意的地方是,由于 GC 可能会移动堆内存上的对象,所以在使用指针操作 GC 堆内存中的对象前,需要先使用 fixed
将其固定:
int[] array = new[] { 1, 2, 3, 4, 5 };fixed (int* p = array){Console.WriteLine(*(p + 3)); // 4}
当然,指针不仅仅局限于对象,函数也可以有函数指针:
delegate* managed<int, int, int> f = &Add;Console.WriteLine(f(3, 4)); // 7static int Add(int x, int y) => x + y;
函数指针也可以指向非托管方法,例如来自 C++ 库中、有着 cdecl
调用约定的函数:
delegate* unmanaged[Cdecl]<int, int, int> f = ...;
进一步我们还可以指定 SuppressGCTransition
来取消做互操作时 GC 上下文的切换来提高性能 。当然这是危险的,只有当被调用的函数能够非常快完成时才能使用:
delegate* unmanaged[Cdecl, SuppressGCTransition]<int, int, int> f = ...;
SuppressGCTransition
同样可以用于 P/Invoke:
[DllImport(...), SuppressGCTransition]static extern void Foo();[LibraryImport(...), SuppressGCTransition]static partial void Foo();
IntPtr
、UIntPtr
、nint
和 nuint
C# 中有两个通过数值方式表示的指针类型:IntPtr
和 UIntPtr
,分别是有符号和无符号的,并且长度等于当前进程的指针类型长度 。由于长度与平台相关的特性,它也可以用来表示 native 数值,因此诞生了 nint
和 nuint
,底下分别是 IntPtr
和 UIntPtr
,类似 C++ 中的 ptrdiff_t
和 size_t
类型 。
这么一来我们就可以方便地像使用其他的整数类型那样对 native 数值类型运算:
nint x = -100;nuint y = 200;Console.WriteLine(x + (nint)y); //100
当然,写成 IntPtr
和 UIntPtr
也是没问题的:
IntPtr x = -100;UIntPtr y = 200;Console.WriteLine(x + (IntPtr)y); //100
SkipLocalsInit
SkipLocalsInit
可以跳过 .NET 默认的分配时自动清零行为,当我们知道自己要干什么的时候,使用 SkipLocalsInit
可以节省掉内存清零的开销:
[SkipLocalsInit]void Foo1(){Guid guid;unsafe{Console.WriteLine(*(Guid*)&guid);}}void Foo2(){Guid guid;unsafe{Console.WriteLine(*(Guid*)&guid);}}Foo1(); // 一个不确定的 GuidFoo2(); // 00000000-0000-0000-0000-000000000000
实际例子熟悉完 .NET 中的部分基础设施,我们便可以来实际编写一些代码了 。
非托管内存在大型应用中,我们偶尔会用到超出 GC 管理能力范围的超大数组(> 4G),当然我们可以选择类似链表那样拼接多个数组,但除了这个方法外,我们还可以自行封装出一个处理非托管内存的结构来使用 。另外,这种需求在游戏开发中也较为常见,例如需要将一段内存作为顶点缓冲区然后送到 GPU 进行处理,此时要求这段内存不能被移动 。
那此时我们可以怎么做呢?
首先我们可以实现基本的存储和释放功能:
public sealed class NativeBuffer<T> : IDisposable where T : unmanaged{private unsafe T* pointer;public nuint Length { get; }public NativeBuffer(nuint length){Length = length;unsafe{pointer = (T*)NativeMemory.Alloc(length);}}public NativeBuffer(Span<T> span) : this((nuint)span.Length){unsafe{fixed (T* ptr = span){Buffer.MemoryCopy(ptr, pointer, sizeof(T) * span.Length, sizeof(T) * span.Length);}}}public void Dispose(){unsafe{// 判断内存是否有效if (pointer != (T*)0){NativeMemory.Free(pointer);pointer = (T*)0;}}}// 即使没有调用 Dispose 也可以在 GC 回收时释放资源~NativeBuffer(){Dispose();}}
经验总结扩展阅读
- 二、.Net Core搭建Ocelot
- 一千零一夜柏海的结局是什么?
- 创建.NET程序Dump的几种姿势
- 中国6大奇葩零食排行榜
- 干无花果怎么吃
- 零食可以托运吗
- C# 8.0 添加和增强的功能【基础篇】
- .NET性能系列文章二:Newtonsoft.Json vs. System.Text.Json
- 某 .NET RabbitMQ SDK 有采集行为,你怎么看?
- .net core Blazor+自定义日志提供器实现实时日志查看器