.NET 零开销抽象指南( 六 )

指针和函数指针指针相信大家都不陌生,像 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();IntPtrUIntPtrnintnuintC# 中有两个通过数值方式表示的指针类型:IntPtrUIntPtr,分别是有符号和无符号的,并且长度等于当前进程的指针类型长度 。由于长度与平台相关的特性,它也可以用来表示 native 数值,因此诞生了 nintnuint,底下分别是 IntPtrUIntPtr,类似 C++ 中的 ptrdiff_tsize_t 类型 。
这么一来我们就可以方便地像使用其他的整数类型那样对 native 数值类型运算:
nint x = -100;nuint y = 200;Console.WriteLine(x + (nint)y); //100当然,写成 IntPtrUIntPtr 也是没问题的:
IntPtr x = -100;UIntPtr y = 200;Console.WriteLine(x + (IntPtr)y); //100SkipLocalsInitSkipLocalsInit 可以跳过 .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();}}

经验总结扩展阅读