.NET性能优化-快速遍历List集合
简介#
System.Collections.Generic.List<T>
是.NET中的泛型集合类,可以存储任何类型的数据,因为它的便利和丰富的API,在我们平时会广泛的使用到它,可以说是使用最多的集合类。
在代码编写中,我们经常需要遍历一个List<T>
集合,获取里面的得元素进行一些业务的处理。通常情况下,集合内的元素不是很多,遍历起来非常快。但是对于一些大数据处理,统计,实时计算等动辄数万、十万数据的List<T>
集合,如何快速的遍历它呢?这就是今天需要和大家分享的内容。
遍历方式#
我们来看看不同遍历方式的性能表现,构建了如下一个性能基准测试,使用不同数量级的集合遍历来看看不同方式的性能表现。代码片段如下所示:
public class ListBenchmark
{
private List<int> _list = default!;
// 分别测试10、1千、1万、10万及100万数据时的表现
[Params(10, 1000, 1_0000, 10_0000, 100_0000)]
public int Size { get; set; }
[GlobalSetup]
public void Setup()
{
// 提前创建好数组
_list = new List<int>(Size);
for (var i = 0; i < Size; i++)
{
_list.Add(i);
}
}
}
使用foreach语句#
foreach
是我们遍历集合是最常用的方式了,它是一个语法糖实现了迭代器模式,它也是作为我们本次的基准。
[Benchmark(Baseline = true)]
public void Foreach()
{
foreach (var item in _list)
{
}
}
因为foreach
语句是一个语法糖,所以最终编译器会使用while
循环调用GetEnumerator()
和MoveNext()
来实现功能。编译后的代码如下所示:
其中MoveNext()
方法实现中会确保在迭代中不会有其它线程修改集合,如果发生了修改则会抛出InvalidOperationException
异常,另外它会有溢出检查,检查当前索引是不是合法的,还需要将对应的元素赋值给enumerator.Current
属性,所以其实它的性能并不是最好的,代码片段如下所示:
我们来看看它在不同集合大小的性能怎么样,结果如下所示:
可以看到在Size
不同的情况下,耗时程线性增长关系,就算是没有任何处理逻辑的遍历100w的数据,则需要至少1s。
使用List的ForEach方法#
另外一个比较常用的方式就是使用List<T>.ForEach()
方法,这个方法允许你传入一个Action<T>
委托,它会在遍历元素时调用Action<T>
委托。
[Benchmark]
public void List_Foreach()
{
_list.ForEach(_ => { });
}
它是List<T>
内部实现的方法,所以能直接访问私有数组,另外能避免掉溢出检查;按照理论上来说它应该会很快速;但是在我们的场景中只有一个空方法,可能表现并不会有完全内联调用的foreach
方法好。
下面是ForEach
方法的源码,可以看到它没有了溢出检查,不过还保留了并发的版本号检查。
另外由于需要给ForEach
方法传递委托,所以在调用代码中,每一次都会检查闭包生成类中的委托对象是否为空,如果不为空则new Action<T>()
,如下所示:
我们来看看它与foreach
关键字相比性能上有什么差别吧。下图是基准测试的结果:
从测试结果来看,要比直接使用foreach
关键字慢40%,看来如非必要,直接使用foreach
是比较好的选择,那么还有没有什么更快的方式呢?
for循环遍历#
回到了我们最古老的方式,就是使用for
关键字来遍历集合。它应该是目前来说性能最好的遍历方式,因为它不需要像之前的那几种方式一样有一些多余的代码(不过索引器同样有检查,防止溢出),另外很显然它不会检查版本号,所以在多线程环境下集合被改变,使用for
不会有异常抛出。测试代码如下所示:
public void For()
{
for (var i = 0; i < _list.Count; i++)
{
// 如果是空循环的话,会被编译器优化
// 我们加一行代码使其不会被编译器优化
_ = _list[i];
}
}
来看看它的结果吧。
这看来就是我们所期待的方式了,直接使用for
循环要比foreach
快60%,原本需要1秒才能遍历完的集合,现在只需要400毫秒。那么还有没有更快的方式呢?
使用CollectionsMarshal#
在.NET5以后,dotnet社区为了让集合操作性能更好,从而实现了CollectionsMarshal
类;这个类里面实现了对于集合类型的原生数组的访问方式(如果你看过我的【.NET性能优化-你应该为集合类型设置初始大小】文章,就知道很多数据结构的底层实现都是数组)。所以它能跳过各种检测,直接访问原始的数组,应该是最快速的。代码如下所示:
// 为了测试编译器有没有针对foreach span优化
// 同时测试for span
public void Foreach_Span()
{
foreach (var item in CollectionsMarshal.AsSpan(_list))
{
}
}
public void For_Span()
{
var span = CollectionsMarshal.AsSpan(_list);
for (int i = 0; i < span.Length; i++)
{
_ = span[i];
}
}
直接访问底层数组是非常危险的,你一定要清楚自己每一行代码在做什么,并且有足够的测试。
基准测试结果如下所示:
Wow,使用CollectionsMarshal比使用foreach要快79%,不过应该是JIT优化的原因,使用foreach
和for
关键字循环Span
没有很大的差别。
总结#
今天和大家聊了聊如何快速的遍历List集合,在大多数的情况下推荐大家使用foreach
关键字,它既有溢出检查也有多线程下版本号的控制,可以让我们更容易的写出正确的代码。
如果在需要高性能和大数据量的场景,那么推荐直接使用for
和CollectionsMarshal.AsSpan
来遍历集合;当然,使用CollectionsMarshal.AsSpan
一定要注意使用方式。
附录#
本文源码链接:https://github.com/InCerryGit/BlogCodes/tree/main/Fast-Enumerate-List
作者:InCerry
作者Github:https://github.com/incerrygit
出处:https://www.cnblogs.com/InCerry/p/Fast-Enumerate-List.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:本博客版权归「InCerry」所有。
.NET性能优化QQ群:687779078(主群)、264167610(备用)
欢迎大家关注我的微信公众号,新文章会优先发到公众号!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库