您的位置:首页 > 其它

并行计算有向无环图和fork/join 框架

2014-05-20 21:04 204 查看
从多任务OS开始,线程主要用来表示IO异步;而今随着4G和多核等的到来,计算密集型又热门起来了。

硬件价格和性能从低到高:

PC/Laptop multi core, memory shared

PC clusters

SuperComputers

假设一个理想并行计算机:每个处理器计算能力相同,忽略调度,

static thread 是对一个虚拟处理器的软件层面的抽象;

static强调的是线程一直存在,不包含线程的创建和销毁。

dynamic multithreded concurrency platform:

通讯协议,负载平衡调度器

spawn a subroutine

parallel loop

并行计算有向无环图

只有三个关键字: parallel, spawn, sync

并行控制指令:spawn, sync, return(包括函数末尾的隐式return)

不包含并行控制的指令序列表示为一个顶点;

顶点u到v的有向边表示指令序列u必须先于v执行; 这时称u,v是串行的,

否则是并行的。

我们把边(u,v)分为:

continuation: 在同一过程实例中,v是u的直接后继。

spawn: 如果u spawn v。

call: 如果u直接调用了v。

return: u返回到调用它的过程,v是这个过程里面紧接sync的。

性能度量

work 是指仅在一个处理器上总的执行时间。如果把一个顶点的执行时间是1,

则work就是顶点总数。

span 是图中最长的路径执行的时间。如果一个顶点花的时间是1,

则span等于图的关键路径长度+1。

Tp记为用p个处理器所花的的总时间。

P*Tp >= T1

T1 是用1个处理器所需的时间,显然,T1 = work。

Tinf 表示用无穷多个处理所需的时间, 显然,Tinf = span。

T1/Tp 称为加速量(speedup)。如果T1/Tp = P, 则称为perfect linear speedup。

T1/Tinf 称为并行度(parallelism)。

T1/(P*Tinf) 称为slackness。

一个好的调度器追求的目标就是越来越接近perfect linear speedup。

贪心调度器:在每个时间步,把尽可能多的顶点分配给处理器。

定理: 使用贪心调度器, Tp <= T1/P + Tinf。

实践中,

slackness 至少是10,则Tinf < 0.1 T1/P,从而Tp <= 1.1 T1/P,对于工程应用已经足够好了。

这里我们看到并不是并行度越大越好。

例1

FIB(n)

if(n <= 1)return n;

else x = spawn FIB(n-1)

y = FIB(n-2)

sync

return x + y;

T1(n) = T(n) = \phi^n;

Tinf(n) = max(Tinf(n-1), Tinf(n-2)) + 1

= n;

容易看到,n值不用太大,就可以达到perfect linear speedup.

例2

MATVEC(A,x)

n = A.rows;

parallel for i = 1 to n

yi = 0;

parallel for i = 1 to n

for j = 1 to n

yi = yi + aij * xj;

return y;

把第二个parallel 用spawn来实现:

MATVEC_LOOP(A, x, y, n, 1, 8);

MATVEC_LOOP(A, x, y, n, i1, i2)

if i1 == i2

for j = 1 to n

yi = yi + aij * xj;

else mid = (i1+i2)/2;

spawn MATVEC_LOOP(A, x, y, n, i, mid);

MATVEC_LOOP(A, x, y, n, mid+1, i2);

sync

spawn 个数对应为二叉树内部节点个数=叶子树n-1,总共有n个叶子,因此额外开销spawn分摊到每个叶子为常量。 每个叶子的计算量此时不能再看做1,而是n。因此

span = lgn + n,

work = n^2,

并行度 = n.

如果对for j 也加上parallel, 则并行度可达到n/lgn。

工程实践:

C/C++ 有 gcc/cilkplus, 因为我下载网上的x64二进制文件, 一直搞不定LD_LIBRARY_PATH,需要从源码编译工具链, 有空的时候练一下。

java 有fork/join framework ,这个框架有什么优点?

当一个fork子问题join等待另一个子问题时,会去偷尚未运行的子问题来执行。

因此每个子问题应该避免使用synchronized,sleep,阻塞IO或过多访问共享变量。

ForkJoinTask类的抽象方法比较多,不适合直接继承,而是继承它的子类RecursiveTask/Action。

子问题执行由ForkJoinPool来调度。

work stealing 如何做到负载平衡?

每个工作线程A都有自己的一个队列。当A划分出一个新任务,把它加入到队列头部(TBD);只有A才可以操作它的队列头部并取出来执行。

当A的队列为空时,它将尝试从另一线程B的队列尾部偷一个任务。

由于这种队列操作特性,是的最大的任务排在队列尾部,从而是的其他线程来偷取实现负载平衡。
一个简短的例子说明多线程的优势:

参考:
[0] 算法导论第三版
[1] 两种任务分发方式的比较: http://stackoverflow.com/questions/7926864/how-is-the-fork-join-framework-better-than-a-thread-pool
[2] concurrent collections 的介绍: http://www.ibm.com/developerworks/cn/java/j-tiger06164/
[3] parallel for: http://stackoverflow.com/questions/4010185/parallel-for-for-java
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: