数据并行(Data Parallelism)

728 篇文章 1 订阅
86 篇文章 0 订阅

数据并行(Data Parallelism)

 

数据并行,不同的数据输入以并行方式运行同一个函数,它把一个任务分解成不连续的单元,因此,可以在单独的线程上并行处理,保证这个任务可以在可用的处理之间进行分配。

通常是处理数据集合,这种方法利用了集合中的项目自然提供了任务分配。最简单的情况,是一个并行的映射函数,对集合的中每一项应用一个转换,结果形成一个新的集合。这种简单的情况通常是可以工作的,是因为集合中的每一项通常可按任意顺序、进行单独处理;用这种处理更复杂的情况,比如汇总列表中所有项,也是可能的;然而,对于一些更复杂的情况,以及必须按顺序处理的,就可能有问题了。

数据并行通常依赖于有并行处理能力的库和框架。虽然它们使用多线程或进程来处理并行,但是,并行并不要求用户创建或控制这些线程;相反,这是库或框架的任务。工作单元可以分布在不同物理机上,以形成计算网格;出于简化的目的,以及由于多核系统已经更为常见和强大,本章将只专注于在一台物理机的多个处理器之间分布工作的系统。微软正致力于在 .NET 框架中提供新的并行编程能力,将在 .NET 框架 4.0 版本中可用;已有其他一些库实现了 .NET 平台上的数据和任务的并行,但是,这一章将只关注.NET 4.0 中的内容。

有两种主要方法完成.NET 4.0 中数据的并行:使用mscorlib.dll 下的System.Threading.Parallel 类,或者使用 System.Core.dll下的 System.Linq.ParallelEnumerable 类。System.Threading.Parallel类完全可以在 F# 中使用,而System.Linq.ParallelEnumerable 类可能是 F# 程序员完成数据并行的首选方法,因为这个库更多地是用函数风格写的。

我们首选看一个示例,它演示如何使用System.Threading.Parallel 类中的并行 For,然后,讨论一下为什么我们并不想用它。假设我们想并行输出 0 到100 的整数,可以用下面的程序:

 

open System.Threading

 

Parallel.For(0, 100, (printfn"%i"))

 

在双核机器上运行时,前面的代码产生的下面的结果:

 

0

13

8

9

6

7

14

10

...

 

来自循环的数以不确定的顺序出现,这是因为以并行方式运行的,人每个函数运行的线程的计划任务是随机的。在 F# 中使用这种函数的问题在某种程度上是由于函数的副作用(side effect)。在 F# 中创建一个有副作用的函数很方便,但是,在并发处理时是不合适的,因为它会引入问题。即使在这个简单的示例中,我们也面对数字不能以原子方式打印的风险,而是混起来打印出来了。例如,你可能看到这样的结果:

 

...

6

174

10

...

 

这里,有两个数字构成了一个三位数,在集合中并不存在。

记住,System.Threading.Parallel 类在有些情况下仍是有用的。假设我们想要发送大量的电子邮件,且想同时发送,就可以用Parallel 类中的并行的ForEach 来并行化这个任务,因为发关电子邮件是输入输出,因此,有副作用:

 

open System.Threading

 

let emails = ["robert@strangelights.com"; "jon@doe.com";

          "jane@doe.com"; "venus@cats.com" ]

 

Parallel.ForEach(emails, (fun addr ->

  //code to create and send email goes here

  ()))

 

即使在这个简单的示例中,也需要保证能够调用任何函数,从多线程在Parallel.For 中调用。

System.Linq.ParallelEnumerable 类更方便 F# 程序的并行化,本质上,它是F# 序列(Seq)模块中函数的并行实现。因为在序列模块与ParallelEnumerable 类之间有大量的名称改变,常见的做法是创建 ParallelEnumerable的薄的包装,这样感觉就像在用序列模块,如下面的代码:

 

namespace Strangelights.Extensions

open System

open System.Linq

 

// Import a small number of functions fromParallelLinq

module PSeq =

  //helper function to convert an ordinary seq (IEnumerable) into aIParallelEnumerable

  letasParallel list: IParallelEnumerable<_> = ParallelQuery.AsParallel(list)

  //the parallel map function we going to test

  letmap f list = ParallelEnumerable.Select(asParallel list, new Func<_,_>(f))

 

  //other parallel functions you may consider using

  letreduce f list = ParallelEnumerable.Aggregate(asParallel list, new Func<_, _,_>(f))

  letfold f acc list = ParallelEnumerable.Aggregate(asParallel list, acc, newFunc<_, _, _>(f))

 

可以使用这段代码的完成版本替代序列模块中的函数调用,簮地换成 PSeq 包装模块的函数调用,在大多数情况下,程序会运行得更快;也有变慢的情况,例如,有些很短的列表,其中每一项要求的工作相对也很小,那么过个代码可能更慢。在微基准测试上可以看到并行映射函数的这种变化,通过比较并行映射与常规映射函数,可以改变输入列表的大小和输入列表中每一项的工作量。

 

注意

微基准测试是一项很有用的工具,有助于了解一段代码的性能,Vance Morrison 在 MSDN 上有一篇关于 .NET 平台上如何运行微基准测试的文章,http://msdn.microsoft.com/en-us/magazine/cc500596.aspx

 

下面的示例演示了你可能的做法:

 

open System.Diagnostics

open Strangelights.Extensions

 

// the number of samples to collect

let samples = 5

// the number of times to repeat each testwithin a sample

let runs = 100

 

// this function provides the"work", by enumerating over a

// collection of a given size

let addSlowly x =

  Seq.fold(fun acc _ -> acc + 1) 0 (seq { 1 .. x })

 

// tests the sequentual map function byperforming a map on a

// a list with the given number of itemsand performing the given

// number of opertions for each item.

// the map is then iterated, to force it toperform the work.

let testMap items ops =

  Seq.map(fun _ -> addSlowly ops) (seq { 1 .. items })

  |>Seq.iter (fun _ -> ())

 

// test the parallel map function, works asabove

let testPMap items ops =

  PSeq.map(fun _ -> addSlowly ops) (seq { 1 .. items })

  |>Seq.iter (fun _ -> ())

 

// a test harness function, takes afunction and passes it the give

let harness f items ops =

  //run once to ensure everything is JITed

  fitems ops

  //collect a list of results

  letres =

    [for _ in 1 .. samples do

     let clock = new Stopwatch()

     clock.Start()

     for _ in 1 .. runs do

       f items ops

     clock.Stop()

     yield clock.ElapsedMilliseconds ]

  //calculate the average

  letavg = float (Seq.reduce (+) res) / (float samples)

  //output the results

  printf"Items %i, Ops %i," items ops

  Seq.iter(printf "%i,") res

  printfn"%f" avg

 

// the parameters to use

let itemsList = [ 10; 100; 200; 400; 800;1000 ]

let opsList = [ 1; 10; 100; 200; 400; 800;1000 ]

 

// test the sequential function

for items in itemsList do

  forops in opsList do

    harnesstestMap items ops

 

// test the parallel function

for items in itemsList do

  forops in opsList do

    harnesstestPMap items ops

 

在查看这个测试结果之前,了解一点微基准测试的知识还是值得的,因为它能更好地帮助我们对析理解。其中最重要的函数可能是harness,它是负责运行测试代码的任务。设置测试任务时,要记住两点:第一,要测量测试结果,每个测试要运行100 次,这是因为在某些小列表上的测试可能运行得异常地快,因此,如果只运行一次,很难测量出它们到底花了多少时间,重复运行就可能避免这个问题;第二,总是创建列表的五个结果,然后取这个列表的平均时间。这是因为在计算机上其他后台进程可能会影响某些测试,多运行几次取平均时间能够避免这个总是,还应该再进一步,计算出标准差(standard deviation),它能够突出那些特别长或特别短的测试结果。

另一个有趣的函数是testMap,它有一个任务,为映射函数提供需要做的工作。需要考虑两个不同的事情:每个输入列表的数目,和列表中每一项需要处理的数量,testMap 函数用两个参数来实现:items 和 ops。参数items 是映射函数必须处理的列表的数目,参数ops 是每一项必须执行的操作的数目。还有一点需要注意的,因为 Seq.map 和 PSeq.map 两个函数都延迟计算(lazy)的,因此,需要强制遍历结果列表。遍历列表将导致延迟序列的创建并计算;如果不这样做,会看到一个很小的、恒定时间的结果,这个是它创建一个能够用来生成列表对象所花费的时间,而不是生成列表本身所花费的时间。通过遍历强制生成列表,使用 Seq.iter 函数。

现在,我们已经可以看看结果本身了(见表 10-1)。

 

表 10-1 微基准测试序列的结果

 

Items

10

 

100

 

200

 

400

 

800

 

Ops

Serial

Parallel

Serial

Parallel

Serial

Parallel

Serial

Parallel

Serial

Parallel

1

1.6

30.6

12

36.2

23.2

42.6

45

88.8

93.2

100

10

2

30.8

33.2

50.8

61.8

74.4

125.4

122.6

251.6

201

100

23.8

39.2

213.4

198.8

421.6

307

822.8

589.6

1660

1024.6

200

40.4

57.8

407.8

299.8

798.8

577.8

1634

1071.2

3262.4

1954.6

400

78.8

94

841

601.8

1676.8

1135.4

3237.4

2228.4

6424.2

3669.2

800

157.2

147.2

1591.4

1095

3174.6

2136.4

6388.4

4238.8

12747.6

7159.8

1000

196.8

181.4

1971.2

1329.6

3966.6

2630.6

7964

5279.6

16026

9111.6

 

解释原始数据是困难的,最好的办法是用这些数字绘制出图表。图 10-3、10-4 和10-5 是 Seq.map 和 PSeq.map 函数处理列表所花时间,以微秒表示,序列的长度是固定的(分别是 10 项、100 项和 1000 项),但是,在序列上的操作数目不同。

图 10-3 处理列表 10 次的时间,有不同的操作数目,以微秒表示

 

图 10-4 处理列表 100 次的时间,有不同的操作数目,以微秒表示

 

图 10-5 处理列表 1000 次的时间,有不同的操作数目,以微秒表示

 

这三个图形很好地说明了试验的结果。对于数量很少的序列(如图 10-3 中的 10项),可以看到,当每一项上执行的操作也少时,并行函数(PSeq.map)比串行函数(Seq.map)慢,随着每一项上执行的操作的增加,并行处理变得比串行的稍快,但差别并不明显;对于 100 项的序列(见图 10-4),可以看到类似的曲线,函数的并行版本比串行版本变快的发生要更提前了,并行函数的增量比串行更明显;最后,对于 1000 项的序列(见图 10-5),可以看到,由函数并行而增加的资源消耗已经完全被抵销了,并行函数线性地比顺序版本快一倍,是因为我们的测试运行在双核处理器上。从这里,我们可以得到结论,使用并行版本的映射函数是值得的,只要输入的列表足够大。

 


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值