2.3.3 小型循环体的特殊处理
如果循环体中只执行了少量的工作,我们或许可以将这些迭代分配到更大的工作单元中去,以谋求更好的性能。这样做是有原因的。当执行一个循环时通常会引入两种类型的成本:管理工作线程的成本和调用函数对象的成本。大多数情况下这些成本是微不足道的,但是,当循环体很小时这些开销就不能忽略不计了。
如果你有很多迭代,而每次迭代的工作量又很小的话,请考虑使用分组策略。
parallel_for的其中一个重载版本允许我们指定索引值的步长(step size),当我们使用步长大于1的迭代时,就可以在并行循环中内嵌一个串行循环。这样一来,外(并行)循环的每一次迭代就由对逐个索引值进行处理转变为对一个索引值范围进行处理。这种将迭代分组的方法可以让我们避免一些并行循环固有的开销。具体的实例代码如下:
- size_t size = results.size();
- size_t rangeSize = size / (GetProcessorCount() * 10);
- rangeSize = max(1, rangeSize);
-
- parallel_for(0u, size, rangeSize,
- [&results, size, rangeSize, workLoad](size_t i)
- {
- for (size_t j = 0; (j < rangeSize) && (i + j < size); ++j)
- results[i + j] = DoWork(i + j, workLoad);
- });
相对于普通无分组的parallel_for函数来说,这种将数据分组的方式会导致更为复杂应用程序的逻辑。一旦遇上大工作量的迭代(或者当迭代区间的大小不均时),这个方法或许就无法帮助我们获得较好的性能了。所以通常只有在经过了深思熟虑之后,或者是在这种循环体极小而迭代次数很多的特定情况下,我们才会考虑采用这种更为复杂的语法。
至于分组的数量,则一般取决于计算机上可用的内核数。默认条件下,分组数量大约维持在内核数的3到10倍是比较理想的。
除此之外,另一种处理这种小型循环体的方法是,调用parallel_for_fixed和parallel_for_each_fixed。这两个函数可以在并发运行时相关的示例程序包中找到。默认情况下,parallel_for和parallel_for_each都具有动态负载平衡(dynamic load balancing)的功能。一旦循环体中的工作量过小,这种负载平衡所带来的开销会对程序产生很大的影响。该示例包中的parallel_for_fixed和parallel_for_each_fixed则不会执行这样的负载平衡,因此它们在小循环体中或许能比parallel_for和parallel_for_each运行得更快一些。
这两组函数的另一个不同之处是,parallel_for和parallel_for_each会检查每一次迭代的注销情况,相比之下,parallel_for_fixed和parallel_for_each_fixed并不会在它们的子区间内执行这种检查。