昨日,写了篇文章《.NET 4:并行求和不爽》,得到大家的热心回复,受大家启发,逐步发现了并行求和效率不要的两个原因:
- 我用的是伪四核 CPU;
- .NET 求和(非并行)效率问题。
今天,就看下 .NET 求和的效率问题。这里不是要和 c、c++等其它语言进行对比,而是对 .NET 中各种求和方式的相对比较。
.NET 中求和有多种方式,可以用 Linq 中的 Sum 扩展方法,也可使用 foreach、for 等。
准备测试数据
先准备用来测试求和的数组:
1 2 3 4 | var random = new Random(); var data = Enumerable.Range(1, 67108864) .Select(i => (long)random.Next(int.MaxValue)) .ToArray(); |
一个很大的长整型数组,装满了随机数,生成大约用 5 秒。
数组求和
我想到的求和方法有下面 5 种,使用 Stopwatch 计时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | Stopwatch watch = new Stopwatch(); //Enumerable.Sum watch.Start(); var sum1 = data.Sum(); watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); //foreach watch.Restart(); var sum2 = 0L; foreach (var d in data) sum2 += d; watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); //for Enumerable.Count watch.Restart(); var sum3 = 0L; for (int i = 0; i < data.Count(); i++) sum3 += data[i]; watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); //for Length watch.Restart(); var sum4 = 0L; for (int i = 0; i < data.Length; i++) sum4 += data[i]; watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); //for 常数 watch.Restart(); var sum5 = 0L; for (int i = 0; i < 67108864; i++) sum5 += data[i]; watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); |
分别是使用 Enumerable.Sum、foreach、for,for 循环又根据结束条件分成了三个。
以下是测试结果:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
Enumerable.Sum | 651 | 706 | 640 | 652 | 640 | 644 | 645 | 669 | 649 | 652 |
foreach | 406 | 431 | 400 | 412 | 403 | 410 | 406 | 409 | 418 | 405 |
for Enumerable.Count | 16532 | 16883 | 17100 | 16668 | 16810 | 17636 | 17413 | 17012 | 17567 | 17655 |
for Length | 341 | 338 | 356 | 349 | 341 | 338 | 304 | 338 | 339 | 337 |
for 常数 | 299 | 293 | 306 | 315 | 298 | 295 | 301 | 296 | 296 | 297 |
(单位:毫秒)
从上面表格中可以看出:
- Enumerable.Count 方式效率最差
- 另外两种 for 循环效率要高于 foreach, 远高于 Enumerable.Sum
列表求和
将前面的代码简单修改,即可用来测试列表 (List<T>)求和
得到的测试结果如下:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
Enumerable.Sum | 766 | 693 | 703 | 974 | 704 | 971 | 699 | 966 | 978 | 751 |
foreach | 698 | 707 | 693 | 682 | 741 | 706 | 686 | 697 | 700 | 709 |
for Enumerable.Count | 1565 | 1570 | 1555 | 1562 | 1559 | 1700 | 1565 | 1684 | 1548 | 1752 |
for Length | 759 | 772 | 761 | 750 | 749 | 788 | 786 | 774 | 747 | 762 |
for 常数 | 609 | 595 | 590 | 588 | 594 | 625 | 592 | 616 | 590 | 598 |
(单位:毫秒)
同样 Enumerable.Count 方式效率最差。
其他几中方式效率相差不太(相对于数组求和)。
总结
同是求和,不同和方式、不同的数据源有不同的效率。
这种效率差异在大规模计算时比较明显,少量计算则可忽略(毫秒级或微秒级的)。
说明:for + Enumerable.Count 的方式之以最慢,是因为每次循环都要调用 Enumerable.Count 方法(可从反编译的 IL 得知)。Enumerable.Count 方法本身效率不错,但经过近七千万次调用,落后 17 秒也是正常的,平均到每次也就 0.25 微秒(毫秒的千分之一)。常规使用,完全可以忽略,希望大家不要对 Enumerable.Count 产生误解。
有了这些测试数据,《.NET 4:并行求和不爽》一文中最后提出的问题也就很容易解答了。