c#大数据循环的优化

2024-04-01

整理一下C#大数据循环的优化

数据准备

初始化数据

for循环和foreach循环对比

我们分别用for循环和foreach循环同样运行一段程序,调整数据量和单次执行时间看看效果如何

 预期耗时For实际耗时单次耗时foreach 实际总耗时单次耗时
总数100、单次200 ms20000 ms20415 ms204.15 ms20373 ms204.73 ms
总数100、单次200 ms20000 ms20538 ms206.87 ms20526 ms205.26 ms
总数100、单次200 ms20000 ms20449 ms204.49 ms20435 ms204.35 ms
总数10000、单次50 ms500000 ms628934 ms62.89 ms628965 ms62.89 ms
总数10000、单次50 ms500000 ms629053 ms62.83 ms630172 ms63.01 ms
总数10000、单次50 ms500000 ms628312 ms62.83 ms629134 ms62.91 ms
总数10000、单次10 ms100000 ms158038 ms15.80 ms157726 ms15.77 ms
总数10000、单次10 ms100000 ms158545 ms15.85 ms158704 ms15.87 ms
总数10000、单次10 ms100000 ms158426 ms15.84 ms161325 ms16.13 ms
总数1000、单次10 ms10000 ms16077 ms16.07 ms15790 ms15.79 ms
总数1000、单次10 ms10000 ms16042 ms16.04 ms15779 ms15.77 ms
总数1000、单次10 ms10000 ms15725 ms15.72 ms15632 ms15.63 ms

我们可以发现几点:

  1. for循环和foreach循环的效率并没有太大差别
  2. 循环的额外耗时跟循环次数和单次循环耗时成正比。一般情况下,循环次数和单次循环耗时数值越大,循环的额外耗时(实际耗时 - 预期耗时)也越大,但是在优化时,这个数值几乎不用考虑(相对而言太小了)

所以在c#中循环效率优化我们可以从循环次数和单次循环耗时这两方面入手

大数据循环优化

在c#中,循环效率优化我们可以从循环次数单次循环耗时这两方面入手

如我们现在有一批数据,总数10000,遍历一次单次耗时50 ms。那么全部遍历完,则至少需要耗时 500000 ms(10000 * 50 ms)

循环次数(多线程)——推荐

提升指数:⭐⭐⭐⭐

一般情况下,for循环是遍历完一次,才继续往下遍历的,相当于在某个时间点只会处理一条数据。那我们能不能让程序在某个时间点处理两条或者三条甚至更多条数据呢?答案肯定是可以的,我们可以利用多线程完成这个操作

如下图,我们可以看到加入多线程整个程序执行耗时减少了将近5倍(628s => 120s)

上述代码开了5个线程,即程序会在在某个时间点处理5条数据,从而提高循环的效率

image-20240328162551060

使用多线程需要注意:

  • 多线程的线程数不是越高越好,具体要看业务场景和机器性能.
  • 多线程的线程安全问题(访问、读取线程外的变量)。使用线程安全的对象如ConcurrentBag、ConcurrentQueue等,访问同一变量需要加锁

单次循环耗时

遍历一次单次耗时 50 ms,那么我们能不能通过程序优化去降低这个耗时呢?答案肯定也是可以,下面列举一下常见的优化方案

避免在循环中访问数据库、读写文件——推荐

提升指数:⭐⭐⭐⭐

如下程序,通过Usre.Id字段查询数据库的用户,对用户进行一系列操作,再将更新后的用户保存到数据库中,大致如下

整个程序的业务逻辑是没有任何问题,但在开发程序中,我们尽量不要在循环中访问数据库。总循环次数少可能看不出来问题,但是随着循环总数量增多问题就非常明显,整个程序不仅耗时,而且还有可能加大数据库的压力,导致数据库崩溃等问题。所以可以的话我们尽量在for循环外层访问读取数据库,如下

显而易见,第二个程序(TestForUpdateByDB)耗时相对要少一点。由于并发的原因,在循环外处理数据可能会出现数据不同步的情况,要注意处理。

image-20240328174127302

避免在循环中使用Count方法——必要

提升指数:⭐⭐⭐⭐⭐

能使用Any方法或者Count属性的地方尽量不要使用Count方法,通常用于判断判断查是否为空序列(没有任何元素)

Count方法属于计数方法,它会将匹配所有的结果进行计数,而Any只要有一条记录满足匹配条件则返回True,如果我们仅仅是判断是否满足某个条件时,优先考虑使用Any方法

image-20240329180343865

避免在循环中使用FirstOrDefault方法

提升指数:⭐⭐

避免在循环中使用First、FirstOrDefault或者Last、LastOrDefault方法。非得使用的话建议使用FirstOrDefault、LastOrDefault方法,因为First、Last方法匹配不到数据会抛异常

我们可以使用字典(Dictionary)来替代FirstOrDefault或者LastOrDefault的效果,如下

我们可以看到这个在for循环中,两者的效率相差较大,而且随着循环数量增大或查询条件增大差距越明显。所以循环大数据时使用字典来代替FirstOrDefault或者LastOrDefault是很有必要的,不过字典没有FirstOrDefault那么"直观"

image-20240329173235607

避免使用Where过滤大数据

提升指数:⭐

避免使用Where方法过滤大量的数据(如在10000总数据中查询出2000条数据)更不要在循环中如此操作,我们可以使用集合操作来代替Where效果。

image-20240329174914456

避免操作大数据量的IEnumerable接口对象——推荐

提升指数:⭐⭐⭐⭐

循环内要避免直接操作大数据量的IEnumerable接口对象,应该在循环外将数据处理成相应的集合类型(ToList、ToDictionary、ToArray)

这也是我为什么会记录这篇文章的原因:一开始,我发现单次循环慢,然后使用字典替代FirstOrDefault查询,用集合操作(Intersect、Except)替代Where查询,执行速度确实有非常明显的提升,我以为是FirstOrDefault和Where的原因,才信心满满来整理,但经过对比才发现真正的主角并不是这俩,而是另有其人,我们先来看看吧。

我们先看一下以下这三个方法的执行顺序

TestForEnumerable1方法的searchNumbers逻辑未执行,因为result变量在程序结束的时候都没有引用(使用),IEnumerable对象只有在使用到这个变量的时候才会开始执行相应具体的逻辑,这种方式在处理大型数据集时可以节省内存和计算资源

执行顺序:开始——>结束

image-20240401104113031

TestForEnumerable2方法的result调用ToList方法,此时会执行searchNumbers逻辑。由于searchNumbers逻辑抛异常未处理,所以没有继续往下执行。

执行顺序:开始——>searchNumbers(异常)——>结束

image-20240401104957486

TestForEnumerable3方法的searchNumbers逻辑(调用了ToList方法)会先执行。由于searchNumbers逻辑抛异常未处理,所以没有继续往下执行。

执行顺序:searchNumbers(异常)——>开始——>结束

image-20240401105600204

经过上面三段代码,我们可以发现,IEnumerable对象对于程序的逻辑顺序会有一定的影响。由于IEnumerable对象的惰性加载(只在需要时加载数据),在有些时候确实能帮助我们提高性能和缓解内存压力,但是在大数据循环与查询(For、Where、FirstOrDefault)中,非常建议大家在此之前将IEnumerable对象处理成相应的集合类型(ToList、ToDictionary、ToArray),这个提升真的很大

我们先来看看FirstOrDefault,两者执行花费的时间差距很大,而且不知道为什么IEnumerable对象查询出来的结果会少于(List)集合对象查询的结果,按理这两种方式应该都是一样的查询结果的

image-20240401143411268

我们先来看看Where查询,三者执行花费的时间差距很大,同样,不知道为什么IEnumerable对象查询出来的结果会多于(List)集合对象查询的结果,按理这三种方式应该都是一样的查询结果的

image-20240401144832006

我们可以看到,查询条件内转为集合类型再查询这种方式是最慢的,因为他每次循环遍历都会执行一次ToList方法。大数据Where查询想要加快查询速度,Where、FirstOrDefault中的查询条件一定要在外部声明变量

image-20240401151452008

大数据循环加快速度正确的使用方式

image-20240401152918076

千万使用下面的方式

image-20240401153220173

回到前话,有时候循环程序执行速度慢,我们先不用急着替换FirstOrDefault、Where,应该优先考虑将这批数据读取到内存中再进行查询筛选操作,再考虑替换FirstOrDefault、Where方法。

优化总结

c#中关于循环总结几点:

  1. for循环和foreach循环的效率并没有太大差别
  2. 大数据循环可以采用多线程的方式,要注意线程安全和机器(cpu)配置。C#常见多线程的实现:Thread、Task、TaskFactory、Parallel并行(有空整理)
  3. 大数据循环可以通过优化程序逻辑减少单次循环的耗时,比较推荐两个优化点:避免在循环中访问数据库读取磁盘等耗时操作,注意数据同步;大数据循环查询时尽量不要直接操作IEnumerable对象,建议先将其加到内存中,注意机器(内存)配置。
  4. 当然,可以将两种方式结合

有时候大数据循环程序执行速度慢,建议按以下优先级进行优化

  • 多线程——推荐 ⭐⭐⭐⭐
  • 避免在循环中访问数据库、读写文件——推荐 ⭐⭐⭐⭐
  • 避免操作大数据量的IEnumerable接口对象——推荐 ⭐⭐⭐⭐
  • 避免在循环中使用Count方法——必要 ⭐⭐⭐⭐⭐
  • 避免在循环中使用FirstOrDefault方法 ⭐⭐
  • 避免使用Where方法过滤大数据