并行程序设计导论 第一章习题

写在前面——
感觉自己又在开坑了。前面《算法导论》和《多处理器编程的艺术》由于理论气息很重,最后题都没有做完。这两天也算是找到了这本《并行程序设计导论》,现在准备从最简单的并发的开始。
笔记应该不会做的太多,除非阅读到某些深有感触或有很大疑惑的地方才会写出来。
还有,对于CSDN博客支持markdown格式也是要赞一个的。第一次用markdown来写博客,很期待其效果。
多说无意,开坑吧。XD

习题1.11

1.1 为求全局总和例子中的my_first_i和my_last_i推导一个公式。需要注意的是:再循环中,应该给各个核分配数目大致相同的计算元素。

提示:先考虑n能被p整除的情况。

解答:
虽然,最后的提示说先考虑整除情况,但非整除的情况也不能不考虑。
这里我们先定义几个变量:
core_id∈[0, p),
num = n/p
rem = n%p

综合考虑写出下面的表达式:
my_first_i(core_id) = num * core_id
my_last_i(core_id) = num * (core_id + 1) + rem

1.2 我们已经隐含地假设每次调用Compute_next_value函数锁执行的工作量与其他次调用Compute_next_value函数执行的工作量大致相同。但是,如果当i=k时调用这个函数的时间是当i=0时调用这个函数所花时间的k+1被,即如果第一次(i=0)调用需要2毫秒,第二次(i=1)调用需要4毫秒,第三次(i=3)调用需要6毫秒,以此类推。那么对于问题1.1,你应该如何回答。

解答:
不知道有没有人对我之前错误的算法感兴趣。
下面,将使用微积分(数形结合)的方式来解这个题目。

首先,计算出每个核上平均的运行时间:
n个任务的总时间(串行) = [2n(n+1)]/2 = n(n+1) ms
因为有p个核,所以每个核上运行的平均时间 = [(n+1)n]/p ms

这里我们可以将任务时间的函数表达式写出来:
f(x) = 2x + 2 ,单位:ms
如果你有初中以上学历的话,你应该能用纸笔画出一条直线。
这条直线与x轴相交在(-1, 0)点,与y轴相交在(0, 2)点。
这里我们在x的正轴方向选取两个点,分别为(x1, 0), (x2, 0),且x1<x2。
然后,可以求出:
f(x1) = 2(x1)+2
f(x2) = 2(x2)+2

现在,求一下x正轴,y正轴,y=x1和任务时间函数直线所形成的面积。
面积是一个直角梯形,就用梯形的面积公式就好:
这里设为A = (2 + (f(x1))) * x1 / 2 = x1(x1 + 2)
同样的,与y=x2围成的面积,
这里设为B = x2(x2 + 2)
不过,这里B不是我们需要的,我们需要算出B中除A之外的面积。
所以,B’ = x2(x2+2) - x1(x1+2)

这里A的面积就相当于其中一个核上执行任务的总时间,B’的面积就箱单关于另一个核上执行任务的总时间。然后,根据题目要求,需要各个核上负载平衡,就可得:
A=B’ => x2(x2+2) - x1(x1+2) = x1(x1 + 2) => 2A = B

根据之前求得的平均时间,可以写出方程:
A=(x1+2)x1=[(n+1)n]/p
B’=x2(x2+2) - x1(x1+2)=[(n+1)n]p=>B=[2(n+1)n]/p

这里求解二元一次方程就很简单了,可以用一般解的表达式去套不同次幂的系数,也可以将方程配成完全平方公式进行求解。这里我就将函数式配成完全平方公式:
x1^2+2(x1)+1=[(n+1)n]/p+1
=>(x1 + 1)^2 = [(n+1)n]/p+1
=>x1={[(n+1)n]/p + 1}^(1/2) - 1

因为2A = B,所以x2={[2(n+1)n]/p + 1}^(1/2) - 1

同理可得,下一个三角形的面积为[3(n+1)n]/p,以此类推,
第k个三角形的面积是[(k)(n+1)n]/p

可得
my_first_i(core_id) = {(core_id + 1)[2(n+1)n]/p + 1}^(1/2) - 1
my_last_i(core_id) = {(core_id + 1)[2(n+1)n]/p + 1}^(1/2) - 2(为了不和下一个核上的第一个任务重合,这里减去了2)
这里暂不讨论小数情况了,如何对小数位进行处理可能会增加解答的长度。

1.3 尝试写出图1.1中的树形结构求全局总和的伪代码。假设核的数目是2的幂(1,2,4,8等)。提示:使用变量divisor来决定一个核应该是发送部分还是接受部分,divisor的初始值为2,并且每次迭代后增倍。使用变量core_difference来决定哪个核与当前核合作,它的初始值为1,并且每次迭代后增倍。例如,在第一次迭代中0%divisor = 0, 1%divisor = 1,所以0号核负责接受和,1号核负责发送。在第一次迭代中0+core_difference = 1, 1-core_difference=0,所以0号核与1号核在第一次迭代中合作。

解答:
这里使用C++的方式,对串行算法直接实现:
这里仅用书中例子作为测试例。

#include <iostream>
#include <math.h>

#define N 8

void foo(int S[]){
    int last;
    for (int divisor = 2, core_difference = 1; divisor <= N; divisor *= 2, core_difference *= 2){
        for (int first = 0; first < N; first += divisor){
            last = core_difference + first;
            S[first] += S[last];
        }
    }
}

int main(){
    //int S[n] = { 8, 19, 7, ... 13, 12, 14 };
    int S[N] = { 8, 19, 7, 15, 7, 13, 12, 14 };
    foo(S);
    for (int i = 0; i < N; i++){
        std::cout << S[i]<< std::endl;
    }
}

1.4 作为前面问题的另一种解法,可以使用C语言的位移操作来实现数形结构的求全局总和。为了了解它是如何工作的,写下核的二进制数编号是非常有帮助的,注意每个阶段互相合作的核。

从表中我们可以看到第一阶段,每个核预期二进制标号的最右位不同编号的核配对,在第二阶段与其二进制编号的最右第二位不同编号的核配对,第三阶段与其二进制编号的最右第三位不同编号的核配对。因此,如果在第一阶段有二进制掩码(bit mask)001(2)、第二阶段有010(2)、第三阶段有100(2),那么可以通过将编号中对应掩码中非零位置的二进制数取反来获得配对核的编号,也即通过异或操作或者^操作。
使用位异或操作或者左移操作编写伪代码来实现上述算法。

解答:
这里感觉文字描述并不是很清楚,所以还是笔算一下感觉才好。
用这里的函数直接替换掉上题中的函数即可。

void foo(int S[]){
    int mask_code = 1;
    int max_level = (int)std::log2(N);
    for (int level_num = 1; level_num <= max_level; level_num++){
        for (int i = 0; i != N; i += std::pow(2, level_num)){
            S[i] += S[i ^ mask_code];
        }
        mask_code <<= 1;
    }
}

1.5 如果核的数目不是2的幂(例如3、5、6、7),那么在习题1.3或者习题1.4中编写的伪代码还能运行吗?修改伪代码,使得在核数目未知的情况下仍能运行。

解答:
这里肯定可以运行,但是结果不正确。
1.3的代码做如下修改

void foo(int S[], int len){
    int last;
    if (len % 2){
        S[0] += S[len - 1];
        len--;
    }
    for (int divisor = 2, core_difference = 1; divisor <= len; divisor *= 2, core_difference *= 2){
        for (int first = 0; first < len; first += divisor){
            last = core_difference + first;
            if (last < len){
                S[first] += S[last];
            }
            else{
                S[0] += S[first];
            }
        }
    }
}

1.4的代码做如下修改:

void foo(int S[], int len){
    int mask_code = 1;
    if (len % 2){
        S[0] += S[len - 1];
        len--;
    }
    int max_level = (int)std::log2(len);
    for (int level_num = 1; level_num <= max_level; level_num++){
        for (int i = 0; i < len; i += std::pow(2, level_num)){
            if ((i ^ mask_code) < len){
                S[i] += S[i ^ mask_code];
            }
            else{
                S[0] += S[i];
            }
        }
        mask_code <<= 1;
    }
}

1.6 在下列情况中,推导公式求出0号核执行接受与加法操作的次数。
a. 最初的全局总和的伪代码。
b. 数形结构求全局总和。
制作一张表来比较这两种算法在总核数是2、4、8、…、1024时,0号核执行的接收与加法操作的次数。

解答:
这里就不作表了,其实把两个算法的次数表达式写出来,大家使用python或matlab等绘制函数图形的软件自然就能看出来次数差别。
A(x)=n-1
B(x)=log(2) [这里log以2为底]

1.7 全局总和例子中的第一部分(每个核对分配给它的计算值求和),通常认为是数据并行的例子;而第一个求全局总和例子的第二个部分(各个核将他们计算出的部分和发送给master核,master核将这些部分再累加求和),认为是任务并行。第二个全局和例子中的第二部分(各个核使用树形结构累加他们的部分和),是数据并行的例子还是任务并行的例子?为什么?

解答:
这里个人认为是任务并行和数据并行结合的例子。与第一个的差别是这里去计算累加和的核要多很多,不过,做的工作都是一样的,所以有任务并行的特点。但其也是任务并行的,因为这里也给每个核分配了一些数据,用于对整体的一部分进行求和。

1.8 假如系里老师要为学生举办一个聚会。
a. 在准备聚会的时候,如何分配各种任务给各个老师,以实现任务并行?设计一个方案使得各个任务能够同时进行。
b. 我们希望其中的一项任务是清理聚会场地,那么该如何分配清扫任务以实现数据并行?
c. 设计一个任务并行和数据并行结合的方案来准备聚会(如果教师的工作量太大,可以让助教来帮忙)

解答:
【引用】
任务并行是指将待解决问题所需要执行的各个任务分配到各个核上执行。
数据并行是指将带解决问题所需要的数据分配给各个核,每个核在分配到数据集上执行大致相似的操作。
a. 将不同的任务分配给各个老师就好。比如,美术老师来画海报,语文老师来写文稿,体育老师进行场地布置等等。
b. 给不同的老师或助教划定一片区域即可,注意一下大家的区域不要重合就好。
c. 比如,聚会需要搭建音乐舞台,有些老师或助教需要去布置灯光,有些老师或助教需要去调音响,有些老师或助教需要去完成音乐舞台的装饰。

1.9 写一篇文章来描述你研究方向中因使用并行计算而获益的事。大致地描述如何使用并行的。你将用到任务并行还是数据并行?

解答:
文章就不写了吧。这里的确是有这样的事。当然,两种并行方式都有用到,不过用的比较多的可能是任务并行。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页