c语言简单的迷宫代码(C高性能低GC非托管动态扩容数组)
相比固定长度的Array,大家可能在编程的时候经常会使用List<T>,同时可能会经常往里面Add东西,因为List具有可扩容性,但是注重GC的朋友会发现(比如Unity开发者),List.Resize会造成扩容前数组长度*泛型类型所占字节长度的GC,同时会造成耗时,以及额外的内存占用(比如List有100个元素的时候触发了扩容,新容量为200,但是总共一共插入了150个元素,导致有50个分配的内存没被利用),我来为大家讲解一下关于c语言简单的迷宫代码?跟着小编一起来看一看吧!
c语言简单的迷宫代码
开始之前相比固定长度的Array,大家可能在编程的时候经常会使用List<T>,同时可能会经常往里面Add东西,因为List具有可扩容性,但是注重GC的朋友会发现(比如Unity开发者),List.Resize会造成扩容前数组长度*泛型类型所占字节长度的GC,同时会造成耗时,以及额外的内存占用(比如List有100个元素的时候触发了扩容,新容量为200,但是总共一共插入了150个元素,导致有50个分配的内存没被利用)
Stream(例如MemoryStream)与List一样,在Resize里会分配当前容量两倍的新byte托管数组,也会造成和上面提到的一样的情况,导致GC和可能存在的额外内存占用,以及拷贝托管数组的耗时。
那么有没有什么办法能实现一个:
- 能插入元素
- 能动态扩容
- 扩容不造成GC
- 能指定扩容长度
包含上述内容的动态扩容数组呢?
让我们先看看List和Stream的原理
List<T>和StreamList<T>和Stream一样,基本是内部有一个托管数组T[]或byte[],
内部会记录当前总容量,以及元素总数,Stream还会额外记录当前的位置
且内部实现了Resize方法,会new一个新的托管数组,长度为当前总容量的两倍
紧接着会把老数组的元素复制到新数组上,老数组不会再被引用且造成GC
Span和Memory最近C#提供了Span和Memory类型,提供了安全操作连续内存的方法
他们的内部实现是这样的:
- 记录对应泛型类型的指针
- 记录该指针的长度(多少个元素)
Span和Memory有一点微小的区别,比如在栈上和托管堆上(Span是ref struct,Memory则是readonly struct的缘故),导致他们的用法不太一样,不过本文只需要关心他们的实现原理。
是不是发现和List<T>以及Stream很像?只是托管数组变成指针了,然后少了一些成员?
指针指针是什么?指针就是一个变量在内存里的地址,所以叫做指针(Pointer),因为指针指向了内存内的一个变量
在内存中的变量有两种情况,一种是被GC托管的变量,一种是不被GC托管的变量,而我们的List和Stream内部的数组,就是托管数组,由GC托管。
如果对Span和Memory熟悉的,应该知道List可以直接转Span,怎么做到的呢?只需要把List内部托管的数组的指针传给Span的构造参数就行(List转Memory也可以就是需要自己实现,有点复杂)
那么延伸的想法就来了,如果我们用非托管指针代替分配的托管数组来存我们的元素,是不是就可以不被GC托管而不被产生GC了?答案是,没错。
自行分配非托管内存如果我们需要申请非托管内存,我们需要实现以下一条很重要原则:
- 手动申请的非托管内存必须用好后手动释放(不然就会造成野指针)
C#有两种方法申请非托管内存,并且任何能运行C#的平台都支持(Unity也是支持的,哪怕是IL2CPP)
- Marshal.AllocHGlobal,该方法会返回指定长度的非托管内存,并且返回的内存有可能会有值
- Marshal.AllocCoTaskMem,该方法会返回至少指定长度的非托管内存,但是也有可能会返回超过改长度的内存,且返回的内存不会有值(全是0)
这里很明显,第一个提到的方法适合我们的使用场景
托管的动态扩容数组类型既然用Sturct可以避免创建时造成的GC(如Span, Memory都是struct),为什么我们要用托管类型(Class)去定义我们的动态扩容数组呢?
请看一下上面提到的原则,手动申请的非托管内存必须用好后手动释放(不然就会造成野指针)
只有通过托管类型,我们才能做到这一点:
- 在构造函数(Constructor内申请非托管内存)
- 在折构函数(Finalizer内释放申请的内存)
折构函数就是一个对象被GC回收前调用的函数)
实现一个非托管类型的动态扩容数组因为非托管类型转指针比较方便,所以本文我们先实现一个非托管类型的动态扩容数组
根据我们上面提到的思路,可以得出以下代码(注,此代码不是完整体):
/// <summary> /// A buffer that can dynamically extend /// </summary> /// <typeparam name="T"></typeparam> public sealed unsafe class ExtensibleBuffer<T> where T : unmanaged { /// <summary> /// Init extensible buffer with a capacity /// </summary> /// <param name="size"></param> /// <param name="InitialData"></param> private ExtensibleBuffer(int size, T[] initialData) { sizeOfT = (byte)sizeof(T); ExpandSize = size; Data = (T*)Marshal.AllocHGlobal(sizeOfT * ExpandSize); if (initialData != null) { fixed(T* ptr = initialData) { copyFrom(ptr, 0, 0, initialData.Length); } } TotalLength = ExpandSize; GC.AddMemoryPressure(sizeOfT * ExpandSize); } /// <summary> /// Free allocated memories /// </summary> ~ExtensibleBuffer() { Marshal.FreeHGlobal((IntPtr)Data); GC.RemoveMemoryPressure(sizeOfT * TotalLength); } }
上面的代码实现了构造函数和析构函数,其中构造函数的参数指定了扩容大小,方法内部获取了泛型T的内存大小,并且申请了类型大小*扩容数量个字节的内存,并且如果有初始化数据,就把初始化托管数据复制到非托管内存上
同时,会标记目前的总长度,以及通知GC我们有申请的内存大小的内存压力(促进GC多去回收)
折构函数内,我们释放了申请的内存,同时通知GC我们之前申请的内存大小的内存压力没了,被我们释放了(让GC不要再关系我们这个动态扩容数组了)
索引器索引器就是数组/List返回指定位置元素的方法:
/// <summary> /// Get element at index /// </summary> /// <param name="index"></param> public T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Data[index]; [MethodImpl(MethodImplOptions.AggressiveInlining)] set { EnsureCapacity(ref index); Data[index] = value; } }
我们在插入的时候检查下申请的内存就好,确保插入到有效的内存里。
实现扩容既然要避免每次扩容都双倍现在的长度从而造成内存浪费,我们需要在构造函数里标记扩容大小,然后每次扩容的时候当前总长度 =扩容大小就好
幸运的是C#提供了一个重新分配通过Marshal.AllocHGlobal申请的内存的方法:
Marshal.ReAllocHGlobal
这个方法需要传两个参数,第一个参数是原申请的指针,第二个参数是新长度(转指针)
通过简单的封装,我们得到了:
/// <summary> /// Ensure index exists /// </summary> /// <param name="index"></param> private void EnsureCapacity(ref int index) { if (index < TotalLength) return; while (index >= TotalLength) { TotalLength = ExpandSize; GC.AddMemoryPressure(sizeOfT * ExpandSize); } Extend(); } /// <summary> /// Extend buffer /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Extend() { Data = (T*)Marshal.ReAllocHGlobal((IntPtr)Data, new IntPtr(TotalLength * sizeOfT)); }
我们只需要定期触发(比如每次插入的时候,访问的话为了性能我们就不检查了,因为是指针,也不会导致数组越界,只是会返回我们想不到的结果)EnsureCapacity,来检查指定的索引是否被我们申请过,如果没的话,就动态扩容以及通知GC即可
从外部的数组/指针里复制元素我们只需要取别的数组/指针,然后从指定偏移开始,复制指定长度到我们申请的指针的指定位置即可:
/// <summary> /// Copy data to extensible buffer /// </summary> /// <param name="src"></param> /// <param name="srcIndex"></param> /// <param name="dstIndex"></param> /// <param name="length"></param> /// <Exception cref="InvalidOperationException"></exception> public void CopyFrom(T[] src, int srcIndex, int dstIndex, int length) { fixed (T* ptr = src) { CopyFrom(ptr, srcIndex, dstIndex, length); } } /// <summary> /// Copy data to extensible buffer /// why unaligned? https://stackOverflow.com/a/72418388 /// </summary> /// <param name="src"></param> /// <param name="srcIndex"></param> /// <param name="dstIndex"></param> /// <param name="length"></param> /// <exception cref="InvalidOperationException"></exception> public void CopyFrom(T* src, int srcIndex, int dstIndex, int length) { var l = dstIndex length; //size check EnsureCapacity(ref l); //copy Unsafe.CopyBlockUnaligned(Data dstIndex, src srcIndex, (uint)length); }
StackOverFlow的这篇文章:stackoverflow.com/a/724证明了不对齐的拷贝内存更快,不过这里我们是非托管类型的非托管内存,所以这样玩不会出问题
复制数据到外部的数组/指针与上面的实现类似,我们只需要获取需要复制到的数组/指针,从我们动态扩容数组的第几个元素开始复制,复制多少个即可
注,这里如果需要复制到指定的数组位置,可以把数组转指针后 偏移,然后调用传指针的方法去复制
转Span
/// <summary> /// Copy data from buffer to dst from dst[0] /// </summary> /// <param name="dst"></param> /// <param name="srcIndex"></param> /// <param name="length"></param> /// <exception cref="OverflowException"></exception> public void CopyTo(ref T[] dst, int srcIndex, int length) { fixed (T* ptr = dst) { CopyTo(ptr, srcIndex, length); } } /// <summary> /// Copy data from buffer to dst from dst[0] /// </summary> /// <param name="dst"></param> /// <param name="srcIndex"></param> /// <param name="length"></param> /// <exception cref="OverflowException"></exception> public void CopyTo(T* dst, int srcIndex, int length) { var l = srcIndex length; //size check EnsureCapacity(ref l); //copy Unsafe.CopyBlockUnaligned(dst, Data srcIndex, (uint)length); }
Span特别有用,在切割内存之类的地方没有什么比Span更适合的了,所以我们顺便把转Span也支持吧
显示转换
/// <summary> /// convert an extensible to buffer from start index with provided length /// </summary> /// <param name="startIndex"></param> /// <param name="length"></param> /// <returns></returns> public Span<T> AsSpan(int startIndex, int length) { var l = startIndex length; //size check EnsureCapacity(ref l); return new Span<T>(Data startIndex, length); }
这样我们可以从指定位置开始讲指定长度个元素转为Span,同时操作返回的Span可以直接操作到我们这个动态扩容数组内的元素上(因为操作Span的元素相当于直接操作内存)
隐式转换
/// <summary> /// Convert to span /// </summary> /// <param name="buffer"></param> /// <returns></returns> public static implicit operator Span<T>(ExtensibleBuffer<T> buffer) => buffer.AsSpan(0, buffer.TotalLength);
这里我们从第0个元素开始把当前总长度个元素转Span
转托管数组因为有可能需要给其他接口使用,所以我们需要能把非托管数组的数据复制到托管数组,只需要new个托管数组然后调用复制的接口即可
完整代码
/// <summary> /// Convert buffer data to an Array (will create a new array and copy values) /// </summary> /// <param name="startIndex"></param> /// <param name="length"></param> /// <returns></returns> public T[] ToArray(int startIndex, int length) { T[] ret = new T[length]; CopyTo(ref ret, startIndex, length); return ret; }
可以在GitHub上看:Nino,当然我本人更希望大家来点star,也可以看下面贴出的代码:
Benchmark
using System; using System.Runtime.InteropServices; using System.Runtime.CompilerServices; namespace Nino.Shared.IO { /// <summary> /// A buffer that can dynamically extend /// </summary> /// <typeparam name="T"></typeparam> public sealed unsafe class ExtensibleBuffer<T> where T : unmanaged { /// <summary> /// Default size of the buffer /// </summary> private const int DefaultBufferSize = 128; /// <summary> /// Data that stores everything /// </summary> public T* Data { get; private set; } /// <summary> /// Size of T /// </summary> private readonly byte sizeOfT; /// <summary> /// expand size for each block /// </summary> public readonly int ExpandSize; /// <summary> /// Total length of the buffer /// </summary> public int TotalLength { get; private set; } /// <summary> /// Init buffer /// </summary> public ExtensibleBuffer() : this(DefaultBufferSize) { } /// <summary> /// Init buffer /// </summary> public ExtensibleBuffer(int expandSize) : this(expandSize, null) { } /// <summary> /// Init extensible buffer with a capacity /// </summary> /// <param name="size"></param> /// <param name="initialData"></param> private ExtensibleBuffer(int size, T[] initialData) { sizeOfT = (byte)sizeof(T); ExpandSize = size; Data = (T*)Marshal.AllocHGlobal(sizeOfT * ExpandSize); if (initialData != null) { fixed(T* ptr = initialData) { CopyFrom(ptr, 0, 0, initialData.Length); } } TotalLength = ExpandSize; GC.AddMemoryPressure(sizeOfT * ExpandSize); } /// <summary> /// Get element at index /// </summary> /// <param name="index"></param> public T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Data[index]; [MethodImpl(MethodImplOptions.AggressiveInlining)] set { EnsureCapacity(ref index); Data[index] = value; } } /// <summary> /// Ensure index exists /// </summary> /// <param name="index"></param> private void EnsureCapacity(ref int index) { if (index < TotalLength) return; while (index >= TotalLength) { TotalLength = ExpandSize; GC.AddMemoryPressure(sizeOfT * ExpandSize); } Extend(); } /// <summary> /// Extend buffer /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Extend() { Data = (T*)Marshal.ReAllocHGlobal((IntPtr)Data, new IntPtr(TotalLength * sizeOfT)); } /// <summary> /// Convert buffer data to an Array (will create a new array and copy values) /// </summary> /// <param name="startIndex"></param> /// <param name="length"></param> /// <returns></returns> public T[] ToArray(int startIndex, int length) { T[] ret = new T[length]; CopyTo(ref ret, startIndex, length); return ret; } /// <summary> /// convert an extensible to buffer from start index with provided length /// </summary> /// <param name="startIndex"></param> /// <param name="length"></param> /// <returns></returns> public Span<T> AsSpan(int startIndex, int length) { var l = startIndex length; //size check EnsureCapacity(ref l); return new Span<T>(Data startIndex, length); } /// <summary> /// Convert to span /// </summary> /// <param name="buffer"></param> /// <returns></returns> public static implicit operator Span<T>(ExtensibleBuffer<T> buffer) => buffer.AsSpan(0, buffer.TotalLength); /// <summary> /// Copy data to extensible buffer /// </summary> /// <param name="src"></param> /// <param name="srcIndex"></param> /// <param name="dstIndex"></param> /// <param name="length"></param> /// <exception cref="InvalidOperationException"></exception> public void CopyFrom(T[] src, int srcIndex, int dstIndex, int length) { fixed (T* ptr = src) { CopyFrom(ptr, srcIndex, dstIndex, length); } } /// <summary> /// Copy data to extensible buffer /// why unaligned? https://stackoverflow.com/a/72418388 /// </summary> /// <param name="src"></param> /// <param name="srcIndex"></param> /// <param name="dstIndex"></param> /// <param name="length"></param> /// <exception cref="InvalidOperationException"></exception> public void CopyFrom(T* src, int srcIndex, int dstIndex, int length) { var l = dstIndex length; //size check EnsureCapacity(ref l); //copy Unsafe.CopyBlockUnaligned(Data dstIndex, src srcIndex, (uint)length); } /// <summary> /// Copy data from buffer to dst from dst[0] /// </summary> /// <param name="dst"></param> /// <param name="srcIndex"></param> /// <param name="length"></param> /// <exception cref="OverflowException"></exception> public void CopyTo(ref T[] dst, int srcIndex, int length) { fixed (T* ptr = dst) { CopyTo(ptr, srcIndex, length); } } /// <summary> /// Copy data from buffer to dst from dst[0] /// </summary> /// <param name="dst"></param> /// <param name="srcIndex"></param> /// <param name="length"></param> /// <exception cref="OverflowException"></exception> public void CopyTo(T* dst, int srcIndex, int length) { var l = srcIndex length; //size check EnsureCapacity(ref l); //copy Unsafe.CopyBlockUnaligned(dst, Data srcIndex, (uint)length); } /// <summary> /// Free allocated memories /// </summary> ~ExtensibleBuffer() { Marshal.FreeHGlobal((IntPtr)Data); GC.RemoveMemoryPressure(sizeOfT * TotalLength); } } }
就这样,我们理论上低GC高性能的非托管动态扩容数组就做好了,让我们分析一下性能,测试代码:
BenchmarkDotNet=v0.13.1, OS=macOS Monterey 12.0.1 (21A559) [Darwin 21.1.0] Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET SDK=6.0.301 [Host] : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT ShortRun : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT Job=ShortRun Platform=AnyCpu Runtime=.NET 6.0 IterationCount=1 LaunchCount=1 WarmupCount=1
首先我们测试了ExtensibleBuffer和List的无优化版(V1,不指定扩容/初始长度),以及优化版(V2,指定扩容/初始长度)
同时我们测试了byte(1字节)作为泛型类型,以及int(4字节)作为泛型类型
我们先看看100个元素的插入:
Method
testCount
Mean
Error
Gen 0
Gen 1
Gen 2
Allocated
ByteExtensibleBufferInsertV1
100
466.7 ns
NA
0.0277
0.0277
0.0277
40 B
ByteExtensibleBufferInsertV2
100
440.4 ns
NA
0.0219
0.0219
0.0219
40 B
ByteListInsertV1
100
273.6 ns
NA
0.0687
-
-
432 B
ByteListInsertV2
100
173.2 ns
NA
0.0253
-
-
160 B
IntExtensibleBufferInsertV1
100
663.2 ns
NA
0.1173
0.1173
0.1173
40 B
IntExtensibleBufferInsertV2
100
645.4 ns
NA
0.0858
0.0858
0.0858
40 B
IntListInsertV1
100
299.2 ns
NA
0.1884
-
-
1,184 B
IntListInsertV2
100
192.1 ns
NA
0.0725
-
-
456 B
为什么会比List略慢?因为申请内存是有耗时的,虽然基本无感知。不过GC的优化是不是挺不错的?
我们现在看看1000个元素的插入:
Method
testCount
Mean
Error
Gen 0
Gen 1
Gen 2
Allocated
ByteExtensibleBufferInsertV1
1000
3,323.6 ns
NA
0.2327
0.2327
0.2327
40 B
ByteExtensibleBufferInsertV2
1000
1,560.3 ns
NA
0.2365
0.2365
0.2365
40 B
ByteListInsertV1
1000
1,917.9 ns
NA
0.3643
-
-
2,296 B
ByteListInsertV2
1000
1,554.6 ns
NA
0.1678
-
-
1,056 B
IntExtensibleBufferInsertV1
1000
3,080.0 ns
NA
0.9689
0.9689
0.9689
41 B
IntExtensibleBufferInsertV2
1000
989.2 ns
NA
0.9251
0.9251
0.9251
41 B
IntListInsertV1
1000
2,445.4 ns
NA
1.3390
-
-
8,424 B
IntListInsertV2
1000
1,868.7 ns
NA
0.6447
-
-
4,056 B
速度是不是基本一样了?但是GC是不是少了特别特别多?
现在看看1000以上的元素的插入:
Method
testCount
Mean
Error
Gen 0
Gen 1
Gen 2
Allocated
ByteExtensibleBufferInsertV1
10000
25,683.9 ns
NA
2.3499
2.3499
2.3499
42 B
ByteExtensibleBufferInsertV2
10000
11,535.0 ns
NA
2.3499
2.3499
2.3499
42 B
ByteListInsertV1
10000
17,051.6 ns
NA
5.2490
-
-
33,112 B
ByteListInsertV2
10000
16,544.9 ns
NA
1.5869
-
-
10,056 B
IntExtensibleBufferInsertV1
10000
25,945.4 ns
NA
9.5825
9.5825
9.5825
46 B
IntExtensibleBufferInsertV2
10000
9,269.5 ns
NA
8.2397
8.2397
8.2397
46 B
IntListInsertV1
10000
23,988.9 ns
NA
20.8130
-
-
131,400 B
IntListInsertV2
10000
16,521.5 ns
NA
6.3477
-
-
40,056 B
ByteExtensibleBufferInsertV1
100000
276,784.6 ns
NA
22.9492
22.9492
22.9492
56 B
ByteExtensibleBufferInsertV2
100000
121,097.0 ns
NA
23.5596
23.5596
23.5596
56 B
ByteListInsertV1
100000
247,649.4 ns
NA
205.3223
205.3223
34.4238
262,583 B
ByteListInsertV2
100000
213,715.3 ns
NA
161.1328
161.1328
26.8555
100,074 B
IntExtensibleBufferInsertV1
100000
244,882.5 ns
NA
93.7500
93.7500
93.7500
109 B
IntExtensibleBufferInsertV2
100000
111,195.8 ns
NA
86.3037
86.3037
86.3037
82 B
IntListInsertV1
100000
533,471.8 ns
NA
619.1406
619.1406
233.3984
1,049,161 B
IntListInsertV2
100000
326,374.4 ns
NA
265.6250
265.6250
99.6094
400,123 B
ByteExtensibleBufferInsertV1
1000000
2,656,296.6 ns
NA
226.5625
226.5625
226.5625
195 B
ByteExtensibleBufferInsertV2
1000000
1,214,632.2 ns
NA
197.2656
197.2656
197.2656
174 B
ByteListInsertV1
1000000
2,422,943.3 ns
NA
1394.5313
1394.5313
398.4375
2,097,906 B
ByteListInsertV2
1000000
1,636,061.4 ns
NA
207.0313
207.0313
197.2656
1,000,185 B
IntExtensibleBufferInsertV1
1000000
3,663,844.0 ns
NA
851.5625
851.5625
851.5625
547 B
IntExtensibleBufferInsertV2
1000000
857,195.9 ns
NA
498.0469
498.0469
498.0469
377 B
IntListInsertV1
1000000
3,717,760.8 ns
NA
1054.6875
1039.0625
1000.0000
8,389,735 B
IntListInsertV2
1000000
2,265,089.4 ns
NA
511.7188
511.7188
492.1875
4,000,381 B
ByteExtensibleBufferInsertV1
10000000
29,853,310.2 ns
NA
1656.2500
1656.2500
1656.2500
1,178 B
ByteExtensibleBufferInsertV2
10000000
10,881,063.5 ns
NA
984.3750
984.3750
984.3750
714 B
ByteListInsertV1
10000000
30,683,668.0 ns
NA
3312.5000
3312.5000
1625.0000
33,556,204 B
ByteListInsertV2
10000000
16,752,229.2 ns
NA
593.7500
593.7500
437.5000
10,000,366 B
IntExtensibleBufferInsertV1
10000000
52,335,791.2 ns
NA
2500.0000
2500.0000
2500.0000
1,802 B
IntExtensibleBufferInsertV2
10000000
8,783,753.0 ns
NA
984.3750
984.3750
984.3750
714 B
IntListInsertV1
10000000
78,802,672.6 ns
NA
5142.8571
5142.8571
3000.0000
134,220,415 B
IntListInsertV2
10000000
33,037,550.0 ns
NA
937.5000
937.5000
937.5000
40,001,345 B
ByteExtensibleBufferInsertV1
100000000
297,324,344.5 ns
NA
5000.0000
5000.0000
5000.0000
3,808 B
ByteExtensibleBufferInsertV2
100000000
113,086,965.2 ns
NA
800.0000
800.0000
800.0000
741 B
ByteListInsertV1
100000000
303,881,242.5 ns
NA
5500.0000
5500.0000
3000.0000
268,438,564 B
ByteListInsertV2
100000000
172,889,432.0 ns
NA
666.6667
666.6667
666.6667
100,002,269 B
IntExtensibleBufferInsertV1
100000000
394,704,429.0 ns
NA
12000.0000
12000.0000
12000.0000
9,536 B
IntExtensibleBufferInsertV2
100000000
77,565,079.3 ns
NA
1000.0000
1000.0000
1000.0000
848 B
IntListInsertV1
100000000
690,861,266.0 ns
NA
8000.0000
8000.0000
3000.0000
1,073,746,576 B
IntListInsertV2
100000000
310,024,197.0 ns
NA
500.0000
500.0000
500.0000
400,001,880 B
速度是不是快了好几倍(毕竟直接在指针上复制会快很多,也少了托管数组分配的耗时),GC是不是少了几千、几万、几十万倍?
使用场景有人可能会问,这玩意儿有使用场景吗?
答案是有的,且很多。
最后
- 基本上用Stream持续写入二进制数据的使用场景都很契合这个非托管动态扩容数组(如网络IO),因为这种IO都是KB/MB/GB级别的,而在这个量级下,该动态扩容数组有着出色的性能和卓越的GC优化
- 序列化这种需要不断写入数据的场所也很契合
- 填充加密的使用情况也很适合(比如把二进制数据每n字节之间插入m字节的假数据,最后再转托管byte数组返回出去,可以用这个动态扩容数组在塞入假数据期间实现无GC高性能处理)
- TCP粘包处理也很契合(类似上面提到的网络IO,但是不太一样,因为要不断地Enqueue二进制数据到扩容数组,然后如果满足包头记录的总长度了,就Dequeue出去,把后面的内容移动到最前面,以后会有这个方案的文章)
- 还有很多很多的用途,比如通过非托管动态扩容数组写数据,然后用其非托管数据的指针,传递给C/C 等原生代码去实现无GC的高性能功能(这个以后也会有文章,关于搭配这个和Zlib native的文章)
为什么不是无GC非托管动态扩容数组呢?因为我们这个数组是个对象,所以造成GC。
特别感谢阅读到最后的朋友,希望能给大家带来帮助,以后我还会写一个收集对象的内存地址,转IntPtr实现的低GC托管动态扩容数组。
来自 开发者傑
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com