简介

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循环要比foreach60%,原本需要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优化的原因,使用foreachfor关键字循环Span没有很大的差别。

总结

今天和大家聊了聊如何快速的遍历List集合,在大多数的情况下推荐大家使用foreach关键字,它既有溢出检查也有多线程下版本号的控制,可以让我们更容易的写出正确的代码。

如果在需要高性能和大数据量的场景,那么推荐直接使用forCollectionsMarshal.AsSpan来遍历集合;当然,使用CollectionsMarshal.AsSpan一定要注意使用方式。

附录

本文源码链接:https://github.com/InCerryGit/BlogCodes/tree/main/Fast-Enumerate-List

.NET性能优化-快速遍历List集合的更多相关文章

  1. for循环实战性能优化之使用Map集合优化

           笔者在<for循环实战性能优化>中提出了五种提升for循环性能的优化策略,这次我们在其中嵌套循环优化小循环驱动大循环的基础上,借助Map集合高效的查询性能来优化嵌套for循环 ...

  2. .NET性能优化-你应该为集合类型设置初始大小

    前言 计划开一个新的系列,来讲一讲在工作中经常用到的性能优化手段.思路和如何发现性能瓶颈,后续有时间的话应该会整理一系列的博文出来. 今天要谈的一个性能优化的Tips是一个老生常谈的点,但是也是很多人 ...

  3. Django数据库性能优化之 - 使用Python集合操作

    前言 最近有个新需求: 人员基础信息(记作人员A),10w 某种类型的人员信息(记作人员B),1000 要求在后台上(Django Admin)分别展示:已录入A的人员B列表.未录入的人员B列表 团队 ...

  4. Java程序性能优化技巧

    Java程序性能优化技巧 多线程.集合.网络编程.内存优化.缓冲..spring.设计模式.软件工程.编程思想 1.生成对象时,合理分配空间和大小new ArrayList(100); 2.优化for ...

  5. SQL Server 性能优化之RML Utilities:快速入门(Quick Start)(1)

      SQL Server 性能优化之RML Utilities:快速入门(Quick Start)(1) 安装Quick Start工具 RML(Replay Markup Language)是MS ...

  6. EntityFramework之原始查询及性能优化(六)

    前言 在EF中我们可以通过Linq来操作实体类,但是有些时候我们必须通过原始sql语句或者存储过程来进行查询数据库,所以我们可以通过EF Code First来实现,但是SQL语句和存储过程无法进行映 ...

  7. 转载:SqlServer数据库性能优化详解

    本文转载自:http://blog.csdn.net/andylaudotnet/article/details/1763573 性能调节的目的是通过将网络流通.磁盘 I/O 和 CPU 时间减到最小 ...

  8. 【转载】 Spark性能优化指南——基础篇

    转自:http://tech.meituan.com/spark-tuning-basic.html?from=timeline 前言 开发调优 调优概述 原则一:避免创建重复的RDD 原则二:尽可能 ...

  9. H5 缓存机制浅析 移动端 Web 加载性能优化

    腾讯Bugly特约作者:贺辉超 1 H5 缓存机制介绍 H5,即 HTML5,是新一代的 HTML 标准,加入很多新的特性.离线存储(也可称为缓存机制)是其中一个非常重要的特性.H5 引入的离线存储, ...

随机推荐

  1. 类型转换——JavaSE基础

    类型转换 类型判断 可以通过Instanceof关键字判断左操作数是否是右操作数的父类或本身 强制类型转换 不能对布尔值进行转换 不能将对象类型转换为不相关的类型 把高容量转向低容量时,需要进行强制类 ...

  2. SpringBoot官方支持任务调度框架,轻量级用起来也挺香!

    大家好,我是二哥呀.定时任务的应用场景其实蛮常见的,比如说: 数据备份 订单未支付则自动取消 定时爬取数据 定时推送信息 定时发布文章 等等(想不出来了,只能等等来凑,,反正只要等的都需要定时,怎么样 ...

  3. mysql调优学习笔记

    性能监控 使用show profile查询剖析工具,可以指定具体的type 此工具默认是禁用的,可以通过服务器变量在绘画级别动态的修改 set profiling=1; 当设置完成之后,在服务器上执行 ...

  4. JS:函数的几种写法1

    1.构造函数: var fn = new function(); 2.声明式: function fn(){}; 3.匿名函数(又称自调用函数): (function(){})(); 4.表达式: v ...

  5. Docker组成原理

    目录 Docker引擎 OCI容器标准 镜像 启动流程 本文是阅读<深入浅出Docker>的相关学习笔记 起初简单的以为Docker和容器是一种东西,后来才发现Docker是实现了Linu ...

  6. 深入理解 happens-before 原则

    在前面的文章中,我们深入了解了 Java 内存模型,知道了 Java 内存模型诞生的意义,以及其要解决的问题.最终我们知道:Java 内存模型就是定义了 8 个基本操作以及 8 个规则,只要遵守这些规 ...

  7. RPA应用场景-账套建立

    所涉人工数量5操作频率 不定时 场景流程 1.客户按照项目开设专项财务管理,每个项目需要在初期建立自己的账套: 2.运营专员通过邮件发送账套建立申请: 3.根据申请进入金蝶运维后台,依据规则完成账套建 ...

  8. 【万字长文】从零配置一个vue组件库

    简介 本文会从零开始配置一个monorepo类型的组件库,包括规范化配置.打包配置.组件库文档配置及开发一些提升效率的脚本等,monorepo 不熟悉的话这里一句话介绍一下,就是在一个git仓库里包含 ...

  9. Lua5.4源码剖析:二. 详解String数据结构及操作算法

    概述 lua字符串通过操作算法和内存管理,有以下优点: 节省内存. 字符串比较效率高.(比较哈希值) 问题: 相同的字符串共享同一份内存么? 相同的长字符串一定不共享同一份内存么? lua字符串如何管 ...

  10. HTTP Status 405 - Request method 'GET' not supported?(尚硅谷Restful案例练习关于Delete方法出现的错误)

    哈罗大家好,最近在如火如荼的学习java开发----Spring系列框架,当学习到SpringMVC,动手实践RESTFUL案例时,发现了以上报错405,get请求方法没有被支持. 首先第一步,我查看 ...