大型Unity/C#游戏中泛型集合过滤的最优性能实现方案问询
三种遍历筛选方式的性能差异
不同运行时环境下三者性能差距差异较大,以下为工业界通用实测结果:
- for循环:性能最高,无额外运行时开销,直接通过索引访问List元素,内存开销仅取决于业务逻辑本身是否需要存储结果,是高频核心逻辑的最优选择。
- foreach循环:针对
List<T>类型时,C#编译器会默认优化为和for循环一致的索引访问逻辑,性能与for循环几乎无差异,仅当遍历接口类型IEnumerable<T>时才会产生枚举器分配开销。 - Linq:
旧版.NET Framework/Unity旧Mono运行时环境下,Linq执行效率比手写循环慢2~10倍,每次调用会产生几KB到几十KB的GC垃圾回收开销,主要来自委托实例、枚举器对象、ToList()生成的新列表分配,高频调用下会造成明显的帧卡顿。
若使用Unity 2021+的.NET 6/7/8新版运行时,.NET官方针对Linq做了大量针对性优化,针对List<T>、数组等常用集合的重载性能比旧版提升3~5倍,简单筛选场景下性能和手写循环差距可缩小到20%以内,但依然存在少量GC开销。
适配游戏场景的最优实现方案
根据调用频率和业务优先级选择不同方案即可平衡性能与开发效率:
- 逐帧执行的高频核心逻辑(比如单位AI调度、碰撞检测筛选、帧级状态统计)
优先使用for/foreach循环,禁止使用原生Linq。
额外优化建议:不要每次筛选都生成新的结果列表,可缓存一个常驻的结果列表对象,每次筛选前调用Clear()再填充元素,完全避免列表内存分配开销。也可将常用筛选逻辑封装为无分配扩展方法,例:public static void FilterLowAmmoUnits(this List<Unit> source, List<Unit> output) { output.Clear(); int threshold = Magazine.MaxCapacity * 3; for (int i = 0; i < source.Count; i++) { var unit = source[i]; if (!unit.IsDead && !unit.IsCriticallyWounded && unit.AmmoCount < threshold) { output.Add(unit); } } } - 事件触发、定时执行的非高频逻辑(比如UI统计显示、任务条件判断、单次事件响应)
可直接使用Linq写法,提升代码可读性与可维护性,该场景下的性能开销完全可以忽略,除非单轮筛选的单位数量超过10万量级。 - 计数场景优化
不要使用Where(cond).Count()的写法,优先手写for循环累加计数,或直接使用List<T>.Count(Func<T, bool>)重载,减少一次遍历开销。 - 超大量单位极端优化方案
若单场战斗单位数量超过1万且存在大量多条件筛选需求,可采用空间换时间的预分组方案:维护对应筛选条件的缓存列表,当单位状态变更时(死亡、受伤、弹药变化、控制权变更),自动更新对应缓存列表的元素,需要使用时直接取缓存列表,完全无需遍历筛选,性能可提升数个数量级。
额外补充说明
如果项目暂时无法升级到新版Unity .NET运行时,又想保留Linq的简洁语法,可使用Unity社区开源的无GC Linq实现,这类库移除了原生Linq的内存分配逻辑,性能接近手写循环,可满足大部分场景需求。
内容的提问来源于stack exchange,提问作者Aaron Carter
相关产品推荐
相关产品推荐

