C#锯齿数组分配性能优化:如何逼近二维数组分配速度?
高效分配固定子数组长度的C#锯齿数组方案
你遇到的核心问题是循环分配大量小内存块导致的GC开销暴增:原锯齿数组方案要创建1亿个独立的8字节小数组,每个数组都要占用托管堆的对象头+数据区,总内存开销是二维数组的4倍还多,且GC要处理1亿个小对象,自然慢得离谱。而二维数组是连续内存块,一次分配完成,没有额外开销。
要在保留byte[][]类型的前提下接近二维数组的分配速度,核心思路是复用连续内存块模拟锯齿数组,以下是最优方案:
方案1:Unsafe连续内存映射(速度接近二维数组)
直接分配一块总长度为1亿*8的连续内存,再通过指针操作让锯齿数组的每个元素指向这块内存的对应偏移位置,全程只做两次大分配,几乎无GC压力。
using System; using System.Runtime.InteropServices; public unsafe byte[][] CreateHashByteFast() { int totalCount = 100000000; int subArrayLength = 8; // 第一步:分配连续大内存块,总大小=总数*子数组长度 byte[] continuousBuffer = new byte[totalCount * subArrayLength]; // 第二步:创建锯齿数组的引用容器 byte[][] jaggedArray = new byte[totalCount][]; // 固定内存地址,避免GC移动 fixed (byte* bufferPtr = continuousBuffer) fixed (byte** jaggedPtr = jaggedArray) { byte* currentOffset = bufferPtr; for (int i = 0; i < totalCount; i++) { // 将锯齿数组的每个元素指向大内存的对应位置 jaggedPtr[i] = currentOffset; currentOffset += subArrayLength; } } return jaggedArray; }
关键说明:
- 需要在项目设置中启用允许不安全代码
- 所有子数组共享同一块连续内存,修改子数组元素等价于修改大数组的对应位置,行为和二维数组完全一致
- 必须保证
continuousBuffer和jaggedArray的生命周期同步,避免大内存被GC回收导致子数组引用失效
方案2:ArrayPool预分配(无需Unsafe,性能次之)
如果不想用unsafe代码,可借助ArrayPool预批量获取8字节数组,减少小对象分配的GC开销:
using System.Buffers; public byte[][] CreateHashByteWithPool() { int totalCount = 100000000; byte[][] jaggedArray = new byte[totalCount][]; var arrayPool = ArrayPool<byte>.Shared; for (int i = 0; i < totalCount; i++) { // 从池中获取8字节数组(池可能返回更大的数组,需确保API能接受,或自行裁剪) jaggedArray[i] = arrayPool.Rent(8); } // 注意:使用完毕后需归还数组到池,避免内存泄漏 // foreach (var arr in jaggedArray) arrayPool.Return(arr); return jaggedArray; }
关键说明:
- 性能比unsafe方案慢,但远优于原循环new的方式
- 池化数组可能长度大于8,若API要求严格8字节长度,可自行封装截断逻辑
原方案慢的根本原因
- 原
new byte[8]循环:每个小数组包含24字节左右的对象头+8字节数据,总内存开销达3.2GB(是二维数组的4倍),且1亿个小对象会让GC陷入巨大压力 Hbyte[]方案:每个Hbyte对象是引用类型,仍需分配1亿个对象实例,加上内部的byte[]引用,内存开销和GC压力依然很大
内容的提问来源于stack exchange,提问作者Major Major
相关产品推荐
相关产品推荐

