如何优化Foreach+Linq.Any实现?超千万级数据性能优化求助
高性能实现:筛选对象首次出现实例的优化方案
背景
当前代码用于筛选满足「PlayerUrl、Position、ChemistryAmount」组合条件的首次出现实例,现有实现存在严重性能瓶颈:处理30万对象耗时10分钟,且每次应用运行需执行该逻辑900次,后续还要处理超1000万级别的数据量,急需优化。
原有数据结构
// 相关属性定义 string PlayerUrl { get; } Position Position { get; } public enum Position { GK, // 其他枚举值 } int ChemistryAmount { get; }
原有低效实现
public static List<IFieldPlayerStatsForPosition> GetFirstFieldPlayerStatsForPositionInList(List<IFieldPlayerStatsForPosition> auxFieldPlayerStatsForPositions) { List<IFieldPlayerStatsForPosition> fieldPlayerStatsForPositions = new List<IFieldPlayerStatsForPosition>(); foreach (IFieldPlayerStatsForPosition fieldPlayerStatsForPosition in auxFieldPlayerStatsForPositions) { if (!fieldPlayerStatsForPositions.Any(cc => fieldPlayerStatsForPosition.FieldPlayerChemistryApplied.FieldPlayer.PlayerUrl == cc.FieldPlayerChemistryApplied.FieldPlayer.PlayerUrl && fieldPlayerStatsForPosition.Position == cc.Position && fieldPlayerStatsForPosition.FieldPlayerChemistryApplied.ChemistryAmount == cc.FieldPlayerChemistryApplied.ChemistryAmount)) { fieldPlayerStatsForPositions.Add(fieldPlayerStatsForPosition); } } return fieldPlayerStatsForPositions; }
性能瓶颈分析
原有实现的核心问题是时间复杂度为O(n²):每次遍历元素时,都要调用Any()线性扫描已保存的结果列表,当数据量达到百万级时,这种嵌套遍历的开销会呈指数级增长,完全无法支撑千万级数据的处理。
优化方案:哈希表替代线性查找
利用HashSet的O(1)平均查找/插入性能,将整体时间复杂度降到O(n),这是最有效的优化方向。以下提供两种可行实现:
方案1:使用ValueTuple作为哈希键(C# 7.0+)
ValueTuple默认实现了正确的Equals和GetHashCode,无需自定义类型,代码简洁高效:
public static List<IFieldPlayerStatsForPosition> GetFirstFieldPlayerStatsForPositionInList(List<IFieldPlayerStatsForPosition> auxFieldPlayerStatsForPositions) { // 提前设置列表容量,避免多次扩容的内存拷贝开销 var result = new List<IFieldPlayerStatsForPosition>(auxFieldPlayerStatsForPositions.Count); var seenKeys = new HashSet<(string PlayerUrl, Position Position, int ChemistryAmount)>(); foreach (var item in auxFieldPlayerStatsForPositions) { // 缓存深层属性,减少重复访问开销 var chemistryApplied = item.FieldPlayerChemistryApplied; var key = ( chemistryApplied.FieldPlayer.PlayerUrl, item.Position, chemistryApplied.ChemistryAmount ); // HashSet.Add()返回bool:true表示键不存在,成功添加 if (seenKeys.Add(key)) { result.Add(item); } } return result; }
方案2:自定义不可变哈希键类型(兼容旧版C#)
如果项目使用的C#版本不支持ValueTuple,可以自定义一个不可变结构体作为哈希键,严格实现相等性和哈希计算:
// 自定义哈希键结构体,保证不可变性和正确的相等判断 public readonly struct PlayerUniqueKey : IEquatable<PlayerUniqueKey> { public readonly string PlayerUrl; public readonly Position Position; public readonly int ChemistryAmount; public PlayerUniqueKey(string playerUrl, Position position, int chemistryAmount) { PlayerUrl = playerUrl; Position = position; ChemistryAmount = chemistryAmount; } public bool Equals(PlayerUniqueKey other) { return PlayerUrl == other.PlayerUrl && Position == other.Position && ChemistryAmount == other.ChemistryAmount; } public override bool Equals(object obj) { return obj is PlayerUniqueKey other && Equals(other); } public override int GetHashCode() { // C# 7.2+ 用内置组合哈希,旧版本可以手动实现 return HashCode.Combine(PlayerUrl, Position, ChemistryAmount); } } // 优化后的筛选方法 public static List<IFieldPlayerStatsForPosition> GetFirstFieldPlayerStatsForPositionInList(List<IFieldPlayerStatsForPosition> auxFieldPlayerStatsForPositions) { var result = new List<IFieldPlayerStatsForPosition>(auxFieldPlayerStatsForPositions.Count); var seenKeys = new HashSet<PlayerUniqueKey>(); foreach (var item in auxFieldPlayerStatsForPositions) { var chemistryApplied = item.FieldPlayerChemistryApplied; var key = new PlayerUniqueKey( chemistryApplied.FieldPlayer.PlayerUrl, item.Position, chemistryApplied.ChemistryAmount ); if (seenKeys.Add(key)) { result.Add(item); } } return result; }
额外优化建议
- 提前设置List容量:结果列表的最大长度不会超过原列表,直接用原列表的Count作为初始容量,避免多次扩容时的内存分配和元素拷贝。
- 缓存深层属性:在循环内将
item.FieldPlayerChemistryApplied缓存到局部变量,减少重复访问嵌套属性的开销(尤其是属性为计算属性时)。 - 避免LINQ嵌套调用:LINQ的
Any()在底层还是线性遍历,对于大数据量场景,手动使用哈希表的性能优势远大于LINQ语法糖。
内容的提问来源于stack exchange,提问作者stephenspcdmav
相关产品推荐
相关产品推荐

