从线性化存储二维/三维数据的数组中提取子数组的高效实现方法
二维线性数组子矩阵提取的问题根源与高性能实现方案
原有实现的越界问题根源
原有实现的索引计算存在本质逻辑错误,直接导致索引偏移触发越界:
- 源数组内的实际X坐标应为「子数组内的局部X值 + 子数组起始位置的X偏移」,原实现错误将偏移量乘以了子数组的X尺寸,错误写成
tileX = x + subPos.x * subSize.x - 源数组内的实际Y坐标应为「子数组内的局部Y值 + 子数组起始位置的Y偏移」,原实现同样错误将偏移量乘以了子数组的Y尺寸,错误写成
tileY = y + subPos.y * subSize.y
对应测试用例的错误演示:当subPos为(1,1)、subSize为(2,2)、originSize为(3,3)时,原逻辑计算Y=1的位置时,tileY = 1 + 12 =3,对应索引为33 +0=9,直接超出0~8的合法索引范围,触发数组越界。
适配Unity DOTS NativeArray的高性能实现
针对NativeArray的无托管内存特性,结合线性数组的存储规则,逐行连续拷贝是理论上的最优实现:因为二维线性数组的存储天然是「行内连续、行间隔开originSize.x个元素」的结构,不存在整块连续的子矩阵内存,无法通过一次memcpy完成拷贝,逐行拷贝连续内存块可以把每一段可拷贝的连续内存的传输效率拉满。
实现逻辑要点:
- 循环外一次性计算固定偏移值,避免循环内重复计算带来的性能开销
- 进入逻辑前完成一次性的边界合法性校验,从根源杜绝越界
- 每一行的拷贝直接调用NativeArray原生的内存拷贝方法,Burst编译后性能与原生memcpy完全一致,无逐元素遍历的额外开销
核心实现代码:
// 边界预校验,从根源避免越界 if (subPos.x < 0 || subPos.y < 0 || subPos.x + subSize.x > originSize.x || subPos.y + subSize.y > originSize.y) { throw new ArgumentOutOfRangeException("子数组范围超出源数组合法边界"); } // 循环外计算固定偏移,避免冗余计算 int baseRowOffset = subPos.y * originSize.x + subPos.x; int rowStride = originSize.x; // 逐行连续内存拷贝 for (int y = 0; y < subSize.y; y++) { int sourceStartIndex = baseRowOffset + y * rowStride; int destinationStartIndex = y * subSize.x; // 原生内存拷贝,无托管开销 subArray.CopyFrom(sourceArray, sourceStartIndex, destinationStartIndex, subSize.x); }
性能说明
- 上述实现完全兼容Burst编译,无托管堆分配,无循环内冗余计算
- 拷贝逻辑走原生无托管内存操作,性能和手动调用memcpy无差异
- 连续内存的拷贝操作是CPU缓存友好的,相比逐元素随机访问的写法,缓存命中率可以大幅提升,在性能敏感链路的优势会更明显
- 没有循环内的分支判断,避免分支预测失效带来的性能损耗
- 三维场景下的实现逻辑完全一致,仅需在循环外层增加对Z维度的遍历,计算源Z维度的行偏移即可,不存在额外的性能损耗
内容的提问来源于stack exchange,提问作者Das Emu
相关产品推荐
相关产品推荐

