并行计算或称平行计算是相对于串行计算来说的。所谓并行计算可分为时间上的并行和空间上的并行。 时间上的并行就是指流水线技术,而空间上的并行则是指用多个处理器并发的执行计算。
并行计算无疑是.Net Framework平台的一大亮点,它自动的将一个任务分解,并以并发的形式执行,程序员不用操心各任务之间的协作和同步问题,这使得可以更加专注于业务的实现。
.NET 中的 TPL(Task Parallel Library),中文意思是任务并行库,它的设计是为了能更简单地编写可自动使用多处理器的托管代码。使用该库,用户可以非常方便地用现有序列代码表达潜在并行性,这样序列代码中公开的并行任务将会在所有可用的处理器上同时运行,通常这会大大提高速度。
但是,从网上很多已经发布的并行计算的例子来讲,有很多存在一定的误区甚至是误导,这导致了一线编程人员产生一些错误的思路,它们多是通过示例讲述并行计算的性能优越性,似乎程序人员可以不费吹灰之力就能将程序性能提升N倍,如果这些想法没有经过比较就应用于实际,那么就会造成一定的损失。这篇文章就来聊聊关于合理使用并行计算的问题,供大家参考,这些误区主要包括:
1. 只要使用并行就会提高程序性能
2. 并行循环嵌套越多程序性能越高
3. 并行计算是运行时的事
下面让我们来一个个的讲解这些误会。
误区一 .只要使用并行就会提高程序性能
实时并不是这样,实际上并行计算的使用对前提要求非常严格,一般情况大量使用并行计算不但不会提升性能,反而会适得其反,下面有两个Case给大家说明。
Case 1. 使用Thread.Sleep()比较并行与单行程序的性能并不客观。
在许多并行计算与单行方式程序性能比较的例子中,很多都包含类似Thread.Sleep()的语句,运行这样的Demo我们确实看到,并行的时间结果竟然提升如此许多,但是你有没有仔细研究一下时间降低的原因呢?
有如下两段代码:
- Code Part A:
- for (int i = 0; i < 10; i++) { a = i.ToString(); Thread.Sleep(200); }
- Code Part B:
- Parallel.For(0, 10, (i) => { a = i.ToString(); Thread.Sleep(200); });
在我的双核本机上,测试结果是令人兴奋的:Code Part A跑了2秒多,而Code Part B只跑了800多毫秒,时间大幅降低,然而这样你就决定将你的代码迁移到并行方式吗?
我建议你还是等等再说吧,Code Part B比A具有更高性能的原因不是因为主代码并行而带来的性能提升,而是由于Sleep(),在并行环境中,任务实际上只是休息了1/N(N为并行数量),而不是单行程序中的全部,这是因为TPL将循环工作分解的缘故,在双核本机上,Code Part B相当于2个5次的循环同时进行,Sleep()又很少有共享资源的消耗,不需要与其他进程同步,所以运行时间比1次10次的循环降低了,假如我们去掉Code Part A、B中的Sleep语句,那么结果又是如何呢?答案是两者十分趋近。实际上在主代码短短小的情况下,并行计算会表现出一定的性能不稳定性,这里留一点,感兴趣的朋友自己测试一下吧。
选择并行计算处理问题,首先要保证你的样本或需求适合并行处理,比如写排序算法时就不能使用并行计算。
Case 2: 并行程序对于字符串与数字的处理效率是不同的。
@ 字符串累加:
单行:
- for (int i = 0; i < 100000; i++) { a += i.ToString(); }
并行:
- Parallel.For(0, 100000, (i) => { a += i.ToString(); });
@ 字符串比较:
单行:
- for (int i = 0; i < 100000; i++) { f = a.Equals(i.ToString()); }
并行:
- Parallel.For(0, 100000, (i) => { f = a.Equals(i.ToString()); });
@ 数字比较:
单行:
- for (int i = 0; i < 100000000; i++) { f = i == j; }
并行:
- Parallel.For(0, 100000000, (i) => { f = i == j; });
运行以上三段测试代码可知,TPL对于字符串处理还是很令人满意的。所以不是所有情况都适合使用TPL库处理程序,这一点对于程序中的遍历情景很重要,在使用PLINQ时,建议先分析样本空间的类型与具体操作,再决定使用哪种计算。
误区二.并行循环嵌套越多程序性能越高
对于两层以上循环的代码段,在你决定使用并行前,先要做的是发现这段代码的主要耗损在哪,是在外层还是在内层,只有评估当并行对性能带来的提升大于损耗时,再重构为并行也不迟,因为TPL在很多情形都提供了并行支持,很多程序员在能用到并行的地方都用并行,而没有经过测试比较,实际上有很多时候,在所有的地方加上并行特性,程序的性能反而会受到损失,这里就不在给出案例了。
从下图可以看出,TPL虽然自动管理了循环中的对象,但是这些“自动”是有一些性能损失的,如果我们的代码中不断地要求TPL进行对象的拆分、合并、同步,而忽视了业务本身的优化,那无疑是对性能不利的。
在这里Aicken给出一个建议,就是在并行前,先要评估这段代码的任务量有多大,有没有必要并行?这段代码有没有对磁盘等“写”要求的竞争?代码是处理什么任务的,以及代码的运行环境是否支持并行;再者就是代码重构后进行性能测试,这样才能保证并行计算有意义。
其实,用对并行计算除了对性能的提升外,还有一点可贵的地方,就是对代码的重构,简洁而富有结构性的代码,更加符合编码进步的要求,不是吗?
【编辑推荐】