You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

3D数组高效遍历算法优化:Godot C#气象模拟性能提升

优化Godot C#中3D体素气象模拟的性能瓶颈

针对你用6层嵌套循环处理16×16×16体素时的性能问题,以下是一系列可落地的优化方案,从内存布局、计算逻辑到并行化全方位降低开销:

1. 内存布局:用SoA替代AoS,改用一维数组

当前你可能用的是包含所有属性的Voxel结构体数组(AoS),且是多维数组,这会导致缓存命中率极低:

  • 多维数组的问题:C#的多维数组是数组的数组,内存不连续,CPU缓存无法高效加载数据。改用一维数组存储,通过索引计算定位体素:
    int sizeX = 16, sizeY = 16, sizeZ = 16;
    int totalVoxels = sizeX * sizeY * sizeZ;
    
    // 一维数组索引计算
    int GetIndex(int x, int y, int z) => x + y * sizeX + z * sizeX * sizeY;
    
  • SoA(数组结构)优化:把每个属性拆成单独的一维数组,而非打包在结构体里。比如压力、温度、方向分量各用一个数组,这样计算某一属性时,只会加载该属性的连续内存,避免缓存加载无关数据:
    float[] pressure = new float[totalVoxels];
    float[] temperature = new float[totalVoxels];
    float[] dirX = new float[totalVoxels];
    float[] dirY = new float[totalVoxels];
    float[] dirZ = new float[totalVoxels];
    

2. 计算逻辑:用"贡献法"替代"邻域遍历求和"

当前每个体素遍历27个邻域求和,导致每个邻域被重复访问多次(比如A的邻域包含B,B的邻域也包含A)。改用贡献法:每个体素主动将自己的属性值加到所有有效邻域的累加器中,最后再取平均,把O(N×27)的复杂度降为更高效的内存友好型遍历:

// 预分配累加数组和邻域计数数组
float[] newDirX = new float[totalVoxels];
float[] newDirY = new float[totalVoxels];
float[] newDirZ = new float[totalVoxels];
int[] neighborCount = new int[totalVoxels];

// 遍历所有体素,向邻域贡献自身方向值
for (int z = 0; z < sizeZ; z++)
{
    for (int y = 0; y < sizeY; y++)
    {
        for (int x = 0; x < sizeX; x++)
        {
            int idx = GetIndex(x, y, z);
            // 遍历27个邻域方向
            for (int dz = -1; dz <= 1; dz++)
            {
                for (int dy = -1; dy <= 1; dy++)
                {
                    for (int dx = -1; dx <= 1; dx++)
                    {
                        int nx = x + dx, ny = y + dy, nz = z + dz;
                        if (nx >= 0 && nx < sizeX && ny >= 0 && ny < sizeY && nz >= 0 && nz < sizeZ)
                        {
                            int nIdx = GetIndex(nx, ny, nz);
                            newDirX[nIdx] += dirX[idx];
                            newDirY[nIdx] += dirY[idx];
                            newDirZ[nIdx] += dirZ[idx];
                            neighborCount[nIdx]++;
                        }
                    }
                }
            }
        }
    }
}

// 计算平均值,替换原数组
for (int i = 0; i < totalVoxels; i++)
{
    if (neighborCount[i] > 0)
    {
        newDirX[i] /= neighborCount[i];
        newDirY[i] /= neighborCount[i];
        newDirZ[i] /= neighborCount[i];
    }
}
(dirX, newDirX) = (newDirX, dirX);
(dirY, newDirY) = (newDirY, dirY);
(dirZ, newDirZ) = (newDirZ, dirZ);

同时,边界体素的有效邻域数可以预先计算并存储,避免每次遍历都做越界判断,进一步节省开销。

3. 向量运算简化

你当前的压力传递公式可以简化,避免不必要的向量运算:

  • 原公式中direction.project(Vector3(x+1,y+1,z+1)).length(),本质是当前方向在邻域方向上的投影长度,直接用点积即可替代:
    // 邻域方向向量(已归一化)
    Vector3 neighborDir = new Vector3(dx, dy, dz).Normalized();
    // 投影长度 = 方向向量 · 邻域方向向量
    float projectionLength = new Vector3(dirX[idx], dirY[idx], dirZ[idx]).Dot(neighborDir);
    // 压力更新
    pressure[nIdx] += projectionLength * pressure[idx];
    
    这样省去了project和length的额外计算步骤,提升单步运算效率。

4. 读写分离,避免数据竞争与缓存失效

不要在遍历过程中直接修改原数组的属性,否则会导致后续体素使用更新后的数据,同时频繁的读写混操作会引发缓存失效。预分配下一帧状态数组,所有计算都写入新数组,完成后再与原数组交换:

  • 比如压力计算时,用newPressure数组存储结果,全部计算完成后再执行(pressure, newPressure) = (newPressure, pressure),确保计算依赖的是上一帧的原始数据,同时让缓存只处理连续的读或写操作。

5. 并行计算利用多核CPU

Godot C#支持.NET的Task并行库,将3D体素数组按切片(比如z轴分层)分配给多个线程并行计算。由于每个体素的计算仅依赖上一帧的原始数据,无数据竞争,可安全并行:

List<Task> tasks = new List<Task>();
int sliceCount = Environment.ProcessorCount;
int sliceSize = sizeZ / sliceCount;

for (int slice = 0; slice < sliceCount; slice++)
{
    int startZ = slice * sliceSize;
    int endZ = slice == sliceCount - 1 ? sizeZ : startZ + sliceSize;
    // 捕获循环变量,避免闭包问题
    int zStart = startZ;
    int zEnd = endZ;
    tasks.Add(Task.Run(() =>
    {
        for (int z = zStart; z < zEnd; z++)
        {
            for (int y = 0; y < sizeY; y++)
            {
                for (int x = 0; x < sizeX; x++)
                {
                    int idx = GetIndex(x, y, z);
                    // 执行当前体素的所有计算(写入new*数组)
                }
            }
        }
    }));
}
Task.WaitAll(tasks.ToArray());

6. 跳过无意义计算

设置阈值过滤微小变化:比如当体素与邻域的压力差小于某个极小值(如0.001)时,跳过方向更新;边界体素可预先存储有效邻域列表,避免每次遍历27个方向都做越界判断。

内容的提问来源于stack exchange,提问作者Hectoris919

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 02:50:51