【问题标题】:Parallel.For partitioningParallel.For 分区
【发布时间】:2013-07-22 10:19:23
【问题描述】:

如何对诸如

之类的东西进行分区
Parallel.For(0, buffer.Length, (i)=> buffer[i] = 0);

我的假设是,对于n 核心机器,工作将被划分为n wayn threads 将执行工作负载。这意味着例如 buffer.Length = 100 and n = 4, each thread will get 0-24, 25-49, 50-74, 75-99 块。 (100 元素数组是说明分区的示例,但请考虑数百万个元素的数组。)

这是一个公平的假设吗?请讨论。

我注意到Array.Clear(...) 在这种特定情况下会执行得更快。你如何合理化这一点?

【问题讨论】:

  • 一个 100 元素的整数数组非常小,可以放在核心的缓存中。此外,通过“清除”之类的操作,您使用的是内存 I/O,而不是 CPU 周期
  • 创建和管理线程的成本比较高。因此,如果每个线程都有相当长的运行任务,这只是一个优势。与创建线程相比,只需将整数变量设置为 0 就“没有”。所以在这种情况下,拥有多个线程的成本远远超过一个线程完成所有工作的成本。
  • @Corak - Parallel 使用池线程,因此您的大部分评论都不适用。
  • 当然 Clear() 更快,它可能会做一个 memset 。错误的测试用例。
  • 在这里你看到了同步的开销......你正在使用最糟糕的例子,并行化的好处是 0

标签: c# .net performance task-parallel-library


【解决方案1】:

首先是简单的部分。一个包含 100 个元素的数组非常小,可以轻松放入内核的缓存中。此外,清除数组相当于将内存区域设置为 0,这可以作为 CPU 命令使用,因此可以尽可能快地完成。

事实上,SSE 命令和并行优化的内存控制器意味着芯片组很可能只使用一个 CPU 命令就可以并行清除内存。

另一方面,Parallel.For 引入了一些开销。它必须对数据进行分区,创建适当的任务来处理它们,收集结果并返回最终结果。在 Parallel.For 下,运行时必须将数据复制到每个内核、处理内存同步、收集结果等。在您的示例中,这可能比将内存位置归零所需的实际时间大得多。

事实上,对于小尺寸,99.999% 的开销很有可能是内存同步,因为每个内核都试图访问相同的内存页面。请记住,内存锁定是在页面级别进行的,您可以在 4K 内存页面中容纳 2K 16 位整数。

至于 PLINQ 如何调度任务 - 使用了许多不同的分区方案,具体取决于您使用的运算符。检查Partitioning in LINQ 以获得不错的介绍。在任何情况下,分区器都会尝试确定是否可以从分区中获得任何好处,并且可能根本不会对数据进行分区。

在您的情况下,分区程序可能会使用远程分区。您的负载仅使用几个 CPU 周期,因此您所看到的只是分区、创建任务、管理同步和收集结果的开销。

更好的基准是在大型阵列上运行一些聚合,例如。计数和平均值等。

【讨论】:

  • +1 用于链接和内存同步点。用于清除内存的 CPU 指令 - 这是推测还是您可以支持该指令?
  • Clear 使用 OS ZeroMemory 功能。检查stackoverflow.com/questions/5557609/… 以获得初学者。至于 CPU 指令,将内存范围设置为单个值是最简单的 SIMD 指令之一
  • SIMD 指令具有特定的数据宽度。他们不能将任意范围的内存设置为值 AFAIK。因此,在这种情况下,仍然需要在循环中读取、设置值和写回内存。但是,这当然比一次设置一个元素要快得多。
  • 除非我弄错了,CLR 不使用 SIMD 指令。
  • 没说有。所述 Array.Clear 使用 (Rtl)ZeroMemory,它位于驱动程序域中。 SIMD 至少是可以清除内存的命令的一个示例。这个 Q 变得太多太宽泛了。
【解决方案2】:

PFX/PLINQ 的优化很复杂。然而,这是基本的图片......

输入端优化:

PLINQ 具有将输入元素分配给线程的三种分区策略:

策略                  元素分配相对表现
块分区         动态                平均      
范围分区         静态                从差到好      
哈希分区          静态                   差      

对于需要比较元素的查询运算符(GroupByJoinGroupJoin 等),PLINQ 总是选择相对低效的哈希分区,因为它必须预先计算每个元素的哈希码(以便元素相同的代码可以在同一个线程上运行)。

对于所有其他查询运算符,您可以选择范围或块分区。默认情况下,如果输入序列是可索引的(如果它是可索引的并且数组继承自IList<T>),PLINQ 将选择范围分区;否则会选择chunk partitioning。

对于每个元素占用相似 CPU 时间的长序列,范围分区更快。否则,块分区会更快。

它们的工作原理:

块分区的工作原理是让每个工作线程定期从输入序列中抓取小“块”元素进行处理。 PLINQ 首先分配非常小的块,然后随着查询的进行增加这个数量;这确保了小序列被有效地并行化,并且大序列不会出现过多的“往返”。如果一个工作线程碰巧很快完成了它的工作,它最终会得到更多的块。该系统使每个线程都同样繁忙,并且机器的核心“平衡”。这种方法的缺点是从共享输入序列中获取元素需要锁定,这会增加开销。

范围分区绕过了正常的输入端枚举,并为每个工作线程预先分配了相等数量的元素,从而避免了输入序列的争用。如果一个线程使用此方法提前完成,它将处于空闲状态,直到其他线程完成。

Parralell ForForeach

默认情况下,对于For/Foreach 循环,PLINQ 将使用范围分区。

我希望这会有所帮助。

【讨论】:

  • “默认情况下,对于 For/Foreach 循环,PLINQ 将使用范围分区。”我认为这是过于简单化了。如果在IEnumerable<T> 上使用ForEach(),则不能使用范围分区,因为事先不知道长度。在这种情况下,将使用块分区。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-02-28
  • 1970-01-01
  • 1970-01-01
  • 2014-10-18
  • 2013-05-27
  • 2014-01-08
  • 1970-01-01
相关资源
最近更新 更多