技术开发 频道

.NET 4.5性能大跨步:任务并行类库应用

  【IT168技术】微软正在努力改进 .NET 4.5 中应用程序的性能,特别是使用任务并行类库(Task Parallel Library)的那些应用。接下来我会带你预览将要完成的改进内容:

  Task, Task<TResult>

  .NET 并行编程API 的核心是Task 对象。对于这样重要的类,微软想法设法保证它要尽可能小。Task 的大多数属性都没有保存在类本身之中,而是保存在另一个名为 ContingentProperties 的对象中。这个二级对象会在程序需要的时候才创建,这样就会降低大多数一般情况下的内存占用。

  .NET 4.0 发布的时候,最常见的情形是分支合并(fork-join)样式的编程,就像我们在 Parallel.ForEach 和 Parallel LINQ 中看到的那样。然而,有了 .NET 4.5 和其中引入的异步机制,顺序样式的编程就取而代之,占据主导地位。微软非常确信这会是主要的方式,因此他们把 ContinuationObject 移动到 Task 中,把其他字段移动到 ContingentProperties 中。这使得顺序结构的代码运行更快,而Task 对象的规模更小。

  Task 也避免了一些不需要的等待。它最初拥有四个属性,但是 Joseph E. Hoag 解释说:由于我们进行了一些很聪明的结构调整,结果只有m_result 字段才是真正必要的。通过对已经存在于基本的 Task 类中的字段重新利用,我们可以废弃m_valueSelector 和m_futureState 字段,而存储在m_resultWasSet 中的信息可以存储在基本类型的上述状态标识中。

        结果创建 Task所需的时间会减少 49-55%,对象的大小会减少 52%。

  Task.WaitAll, Task.WaitAny

  试想一下,我们需要同时等待十亿个任务。在一台 x64 的计算机上,这会导致 12,000,000比特的负载,这还没有计算任务本身。如果使用 .NET 4.5,负载会降到仅仅 64 比特。同时 WaitAny 的负载也会从 23,200,000比特降到 152 比特。

  之所以出现如此戏剧化的效果,是因为微软改变了使用核心同步基元(kernel synchronization primitives)的方式。在之前的版本中,每个任务都需要一个基元(primitive )。现在已经大大减少,每个等待操作只需要一个基元,与操作中的任务数量无关。

  ConcurrentDictionary

  在 .NET 中,只有引用类型和很小的值类型才能够以原子的方式赋值。较大的值类型——像 Guid——则无法以原子的方式读写。在 .NET 4.0 中,为了解决这个问题,ConcurrentDictionary 会使用 node 对象,每次与键值关联的值发生改变的时候,都会重新创建这个对象。在 .NET 4.5 中,只有在无法以原子的方式对值进行写操作的时候,才会创建新的 node 对象。

  另一项改变是我们可以动态地创建锁。Igor Ostrovsky 写到:在实践中,为了达到最大吞吐量,往往需要大量锁。另一方面,我们又不希望分配太多锁对象,特别是在 ConcurrentDictionary 最后只存储了很少项目的时候。

  想要提升性能,就要减少内存分配

  Joseph 写到:在我们的评测结果中你可以看到,在测试中分配的内存数量和完成测试所需的时间之间有直接关系。当我们单独查看的时候,内存分配并不是非常昂贵。但是,当内存系统只是偶尔清理不使用的内存时,问题就出现了,并且问题出现的频率和要分配的内存数量成正比。因此,你分配越多的内存,对内存进行垃圾回收的频率就越频繁,你的代码性能就会变得越差。

  想要降低内存使用,一种方式就是避免使用闭包(closure)。不要在匿名的函数中捕获局部变量,我们可以把它传递给 Task 的构造函数,作为它的“状态(state)对象”。从 .NET 4.5 开始,Task.ContinueWith 也会支持状态对象。

  另一种减少内存使用的技术是缓存经常使用的任务。例如,假设一个函数会接受一个数组作为参数,并返回 Task。因为对于空数组结果总会是一样的,所以缓存代表空数组的 Task 就很合理。

  下一个技巧是避免让任务不必要地“膨胀”。当某些代码触发了创建 ContingentProperties 的操作,Task 对象就会膨胀。最经常出现的原因包括:

  ①创建的任务带有 CancellationToken

  ②任务是从非默认的 ExecutionContext 创建的

  ③Task 作为父 Task 参与到“结构化并行机制(structured parallelism)”中

  ④Task 以 Faulted 状态结束

  ⑤Task 通过((IAsyncResult) Task) .AsyncWaitHandle.Wait ()处于等待状态

  大家还要记住,任务膨胀并不一定是坏事。它只是需要注意的问题,这样我们就不会做不需要的事情,像传入从来不会用到的 CancellationToken 等。

  英文原文:Task Parallel Library Improvements in .NET 4.5

0
相关文章