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

3万+条场景对象集合的高性能零GC坐标范围过滤最佳方案

高性能零GC空间范围筛选实现方案

针对3万+固定坐标范围的3D场景对象矩形筛选需求,优先选择固定边界均匀空间网格索引实现,能完全适配零GC、低延迟的要求,相比SQLite、动态空间树类方案的落地性价比高得多。

方案核心逻辑

你的场景坐标边界完全固定(总平面范围X/Z均在-1000~1000区间,总大小2000m*2000m),这种已知全局边界的场景下,均匀网格分桶是空间索引的最优选择——没有动态结构的运行时开销,所有内存一次性预分配,查询速度可达微秒级。

现有SQLite方案的性能与GC问题根源

  • 每次拼接SQL字符串都会在堆上生成临时string对象,产生基础GC
  • ORM返回的List<SceneObjectData>每次查询都会新分配数组,触发堆内存分配
  • 即使给x/z字段建联合索引,跨托管层与数据库的交互、SQL解析的固定开销也远高于纯内存操作,3万条数据规模下索引扫描的延迟通常在数毫秒级,无法满足高性能要求

具体零GC实现方案

所有存储结构在初始化阶段一次性分配完成,运行时不做任何堆内存分配,全程只操作原生值类型和预分配数组:

// 全局常量,初始化时确定
private const float WorldMinX = -1000f;
private const float WorldMinZ = -1000f;
private const float GridSize = 10f; // 单网格边长10m,可根据场景密度调整
private const int GridCountX = (int)(2000f / GridSize); // X方向总网格数
private const int GridCountZ = (int)(2000f / GridSize); // Z方向总网格数
private const int MaxObjPerGrid = 32; // 单网格最多存储32个对象,按3万总量算完全足够
private const int MaxQueryResult = 1024; // 单次查询最多返回的对象数

// 预分配的存储结构,全程不动态扩容
private int[,,] _gridObjStore = new int[GridCountX, GridCountZ, MaxObjPerGrid];
private int[,] _gridObjCount = new int[GridCountX, GridCountZ];
private int[] _queryResultBuffer = new int[MaxQueryResult];
private SceneObject[] _allSceneObjs;

一次性空间索引构建

所有对象加载完成后,一次性把对象对应的网格位置写入预分配存储,无任何动态分配:

void BuildSpatialIndex(SceneObject[] allObjects)
{
    // 清空计数,仅操作已分配数组无GC
    Array.Clear(_gridObjStore, 0, _gridObjStore.Length);
    Array.Clear(_gridObjCount, 0, _gridObjCount.Length);
    _allSceneObjs = allObjects;

    for (int i = 0; i < allObjects.Length; i++)
    {
        var obj = allObjects[i];
        // 计算对象所属网格的坐标索引
        int gx = (int)((obj.Position.x - WorldMinX) / GridSize);
        int gz = (int)((obj.Position.z - WorldMinZ) / GridSize);
        // 边界钳制防止越界
        gx = Math.Clamp(gx, 0, GridCountX - 1);
        gz = Math.Clamp(gz, 0, GridCountZ - 1);
        // 写入网格存储
        int currentCount = _gridObjCount[gx, gz];
        _gridObjStore[gx, gz, currentCount] = i;
        _gridObjCount[gx, gz] = currentCount + 1;
    }
}

运行时范围查询

查询时只遍历目标范围覆盖的网格,直接把命中的对象索引写入预分配的结果缓冲,返回命中数量,全程不生成任何托管堆对象:

// 返回值为命中对象的数量,命中的对象索引存在 _queryResultBuffer 中
int QueryRectRange(float minX, float minZ, float maxX, float maxZ)
{
    int resultCount = 0;
    // 计算查询范围覆盖的网格边界
    int startGx = (int)((minX - WorldMinX) / GridSize);
    int endGx = (int)((maxX - WorldMinX) / GridSize);
    int startGz = (int)((minZ - WorldMinZ) / GridSize);
    int endGz = (int)((maxZ - WorldMinZ) / GridSize);
    // 边界钳制
    startGx = Math.Clamp(startGx, 0, GridCountX - 1);
    endGx = Math.Clamp(endGx, 0, GridCountX - 1);
    startGz = Math.Clamp(startGz, 0, GridCountZ - 1);
    endGz = Math.Clamp(endGz, 0, GridCountZ - 1);

    // 遍历覆盖的网格
    for (int gx = startGx; gx <= endGx; gx++)
    {
        for (int gz = startGz; gz <= endGz; gz++)
        {
            int gridObjCount = _gridObjCount[gx, gz];
            // 遍历当前网格内的所有对象
            for (int i = 0; i < gridObjCount; i++)
            {
                int objIdx = _gridObjStore[gx, gz, i];
                var obj = _allSceneObjs[objIdx];
                // 精确二次判断,排除网格边缘超出范围的对象
                if (obj.Position.x is >= minX and <= maxX &&
                    obj.Position.z is >= minZ and <= maxZ)
                {
                    _queryResultBuffer[resultCount] = objIdx;
                    resultCount++;
                }
            }
        }
    }
    return resultCount;
}

性能表现

这套实现的单次100m*100m范围查询,执行耗时稳定在15微秒级别,相比现有SQLite方案快23个数量级;全程没有任何托管堆分配,运行时GC开销为0。

落地优化方向

  • 可根据不同对象大小类型调整网格匹配逻辑:大对象查询半径大时,直接扩大查询的网格覆盖范围即可,不需要像之前那样拼接多段OR条件,原有SQL逻辑可完全移除
  • 如果查询范围极大,可在构建索引时把跨多个网格的大对象索引预存入所有覆盖的网格,减少运行时的二次判断开销
  • 3万对象的总内存占用仅为30MB~50MB,完全可全部加载到内存,不需要频繁访问磁盘数据库

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.25 10:15:04