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

3.1
在问候程序中,如果strlen(greeting)代替strlen(greeting)+1来计算进程1、2、…、comm_sz-1发送消息的长度,会发生什么情况?如果用MAX_STRING代替strlen(greeting)+1又会是什么结果?你可以解释这些结果吗?

解答:
①这样的替换并不影响显示,不过传递的是两个不大一样的字符串。
【引用 58页】在我们的程序,参数msg_size是消息字符串加上C语言中字符串结束符\0所占的字符数量。参数msg_type的值是MPI_CHAR。这两个参数一起告知系统整个消息有strlen(geeting)+1个字符。
②这个问题与上面的问题完全相反。这里MAX_STRING是绝对大于strlen(greeting)+1的,所以传递过去的字符数量会是MAX_STRING,会将接收区的缓存占满。不过,对于打印输出来说,并没有什么影响。

3.2
改变梯形积分法,使其能够在comm_sz无法被n整除的情况下,正确估算积分值(假设n≥comm_sz)

解答:

#include <stdio.h>
#include <mpi.h>

double f(double x){
  return x*x;
}

double Trap(
  double left_endpt,   /*  in  */
  double right_endpt,  /*  in  */
  int trap_count,      /*  in  */
  double base_len      /*  in  */
  ){
  double estimate, x;
  int i;

  estimate = (f(left_endpt) + f(right_endpt))/2.0;
  for (i = 0; i < trap_count - 1; i++){
    x = left_endpt + (i + 1) * base_len;
    estimate += f(x);
  }
  return estimate * base_len;
}

int main(){
  int my_rank, comm_sz, n = 64, local_n;
  double a = 0.0, b = 3.0, h, local_a, local_b, remain = 0.0;
  double local_int, total_int;
  int source;

  MPI_Init(NULL, NULL);
  MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
  MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);

  //Get_data(my_rank, comm_sz, &a, &b, &n);

  h = (b - a)/n; //h is the same for all processes

  local_n = n/comm_sz; // So is the number of trapezoids

  local_a = a + my_rank * local_n * h;
  local_b = local_a + local_n * h;
  local_int = Trap(local_a, local_b, local_n, h);



  if (my_rank != 0){
    MPI_Send(&local_int, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD);
  } else {

    total_int =local_int;
    for (source = 1; source < (comm_sz); source++){
      MPI_Recv(&local_int, 1, MPI_DOUBLE, source, 0, MPI_COMM_WORLD,
               MPI_STATUS_IGNORE);
      total_int += local_int;
    }

    // 这里是因为有一些矩阵没有计算到,所以,非整除的情况下结果会有比较大的缺失。
    // 这里将这些矩形在0号线程上补上。
    if (n%comm_sz){
      remain = Trap(h * local_n * comm_sz, b, n%comm_sz, h);
    }
    
    total_int += remain;
  }

  if (my_rank == 0){
    printf("With n = %d trapezoids, our estimate\n", n);
    printf("of the integral from %f to %f = %.15e\n", a, b, total_int);
  }

  MPI_Finalize();
  return 0;
}

3.3
梯形积分法程序中那些变量是局部的,那些是全局的?

解答:
【引用63页】注意,我们对标识符的选择,是为了区分局部变量与全局变量。局部变量只在使用它们的进程内有效。梯形积分法程序中的例子有:local_a,local_b和local_n。如果变量在所有进程中都有效,那么该变量就称为全局变量。该程序中的例子有:变量a,b和n。这与你在编程导论课上学到的用法不同。在编程导论课上,局部变量是指单个函数的私有变量,而全局变量是指所有函数都可以访问的变量。

3.4
mpi_output.c程序中,每个进程只打印一行输出。修改程序,使程序能够按进程号的顺序打印,即,进程0先输出,然后进程1,一次类推。

解答:
其实这里的实现和3.1中的问候程序差不多。

#include <stdio.h>
#include <string.h>
#include <mpi.h>

int main(){
  int my_rank, comm_sz, q;
  char message[100];

  MPI_Init(NULL, NULL);
  MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
  MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

  //printf("Proc %d of %d > Does anyone have a toothpick?\n", my_rank, comm_sz);

  if (my_rank != 0){
    sprintf(message, "Proc %d of %d > Does anyone have a toothpick?", my_rank, comm_sz);
    MPI_Send(message, strlen(message)+1, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
  } else {
    printf("Proc %d of %d > Does anyone have a toothpick??\n", my_rank, comm_sz);
    for(q = 1; q < comm_sz; q++){
      MPI_Recv(message, 100, MPI_CHAR, q, 0,MPI_COMM_WORLD, MPI_STATUS_IGNORE);
      printf("%s\n", message);
    }
  }

  MPI_Finalize();
  return 0;
}

3.5
二叉树中有着从根到每个节点的最短路径,这条路径的长度称为该节点的深度。每个非叶子节点结点都有2个子节点深度都相同的满二叉树称为完全二叉树。用数学推导证明:如果T是一颗有着n个叶子节点的完全二叉树,那么叶子节点的深度为log(n)【底为2】

解答:
当n=1 => 0=log(1)时显然。
假设当T具有n个叶子结点时的完全二叉树的深度为log(2),则
当n=2k(以及2(k+1),…,2(k+p))时,由归纳假设知前2k个结点构成深度为log(n)的树,再由完全二叉树的定义知剩余的1(或2,…,2^k)个结点均填在第log(n)-1层上,故深度刚好增加了1。
故n<=2^(k+1)时命题成立。证毕。
(首先最好能先从直观上理解:完全二叉树中:
1层 有1个叶子结点;
2层 有2个叶子结点;
3层 有4个叶子结点;
……
k层 有2^(k-1)个叶子结点;)

3.6
假设comm_sz=4,x是一个拥有n=14个元素的向量
a 如何用块划分法在一个程序的进程间分发x的元素
b 如何用循环划分法在一个程序的进程间分发x的元素
c 如何用b=2大小的块用块-循环划分法在一个程序的进程间分发x的值
你的分配方法应该通用性足够强,无论comm_sz和n取何值都能分配。你应该使你的分配“公正”,如果q和r是任意的2个进程,分给q和r的x分量个数的差应尽可能小。

解答:
a 块划分

进程0  | 0  1  2  3
进程1  | 4  5  6  7
进程2  | 8  9  10 11
进程3  | 12 13

b 循环划分

进程0  | 0  4  8  12
进程1  | 1  5  9  13
进程2  | 2  6  10
进程3  | 3  7  11

c 块-循环划分

进程0  | 0  1  8  9  
进程1  | 2  3  10 11  
进程2  | 4  5  12 13
进程3  | 6  7

3.7
如果通信子只包含一个进程,不同的MPI集合通信函数分别会做什么。

解答:
这个验证起来很简单,就是将书中的程序,使用mpicc编译后,直接运行二进制文件就行了。
可以看到的是,所有的操作就相当于这个进程发送信息给自己,然后完成对应的操作。
个人感觉,当只有一个进程存在在通信子中时,就没有必要使用MPI来进行通讯了。

3.8
假定comm_sz=8, n=16
a 画一张图来说明进程0要分发n个元素的数组,怎样使用拥有comm_sz个进程的树形结构的通信来实现MPI_Scatter。
b 画一张图来说明原先分布在comm_sz个进程间的n个数组元素由进程0保存,怎样使用树形结构的通信来实现MPI_Gather.

解答:
这里个人感觉在细看一下73和74页的内容,就能知道这两个函数,是如何进行分配的了。
至于这里说的树形结构,个人没弄明白是怎么样的一个图。

3.9
编写一个MPI程序实现向量与标量相乘以及向量点积的功能。用户需要输入2个向量和1个标量,都有进程0读入并分配给其他进程,计算结果由进程0计算和保存,并最终由进程0打印出来。假定向量的秩n可以被comm_sz整除。

解答:

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>

#define N 4

void Read_vector(
  double vector_a[],
  double vector_b[],
  double *scalar,
  int local_n,
  int n,
  int my_rank,
  MPI_Comm comm){

  double va[N], vb[N];
  int i;

  if (my_rank == 0){
    printf("Enter the vector_a:\n");
    for (i = 0; i < n; i++){
      scanf("%lf", &va[i]);
    }

    printf("Enter the vector_b:\n");
    for (i = 0; i < n; i++){
      scanf("%lf", &vb[i]);
    }

    printf("Enter the scalar:\n");
    scanf("%lf", scalar);

    MPI_Scatter(va, local_n, MPI_DOUBLE, vector_a, local_n, MPI_DOUBLE, 0, comm);
    MPI_Scatter(vb, local_n, MPI_DOUBLE, vector_b, local_n, MPI_DOUBLE, 0, comm);
  } else {
    MPI_Scatter(va, local_n, MPI_DOUBLE, vector_a, local_n, MPI_DOUBLE, 0, comm);
    MPI_Scatter(vb, local_n, MPI_DOUBLE, vector_b, local_n, MPI_DOUBLE, 0, comm);
  }
}


void vect_mult(
  double vector_a[],
  double vector_b[],
  double scalar,
  double v1_v2_buf[],
  double v1_s_buf[],
  double v2_s_buf[],
  int local_n,
  int n,
  MPI_Comm comm){

  double va[N], vb[N];
  int i;

  MPI_Allgather(vector_a, local_n, MPI_DOUBLE, va, local_n, MPI_DOUBLE, comm);
  MPI_Allgather(vector_b, local_n, MPI_DOUBLE, vb, local_n, MPI_DOUBLE, comm);

  for (i = 0; i < n; i++){
    v1_v2_buf[i] += va[i] * vb[i];
    v1_s_buf[i] += va[i] * scalar;
    v2_s_buf[i] += vb[i] * scalar;
  }
}

int main(){
  int my_rank, comm_sz, n = N, local_n;
  double vector1[N] = {0}, vector2[N] = {0}, scalar = 0.0, v1_v2 = 0.0;
  double v1_v2_buffer[N], v1_s_buffer[N], v2_s_buffer[N];

  MPI_Init(NULL, NULL);
  MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
  MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);

  local_n = n / comm_sz;

  Read_vector(vector1, vector2, &scalar, local_n, n, my_rank, MPI_COMM_WORLD);

  vect_mult(vector1, vector2, scalar, v1_v2_buffer, v1_s_buffer, v2_s_buffer, local_n, n, my_rank, MPI_COMM_WORLD);

  int i = 0;
  if (my_rank == 0){
#if 0
// just for debug
    printf("\nvector1 is :\n");
    for (i = 0; i < n; i++){
      printf("%g, ", vector1[i]);
    }

    printf("\nvector2 is :\n");
    for (i = 0; i < n; i++){
      printf("%g, ", vector2[i]);
    }

    printf("\nscalar is %g\n", scalar);

    for(i = 0; i < n; i++){
      printf("v1_v2_buffer[%d] is %g\n", i, v1_v2_buffer[i]);
      v1_v2 += v1_v2_buffer[i];
    }
#endif
    printf("\nvector1 * vector2 is %lf.\n", v1_v2);

    printf("\nvector1 * scalar:\n");
    for (i = 0; i < n; i++){
      printf("%lf ", v1_s_buffer[i]);
    }

    printf("\n\nvector2 * scalar:\n");
    for (i = 0; i < n; i++){
      printf("%lf ", v2_s_buffer[i]);
    }

    printf("\n");
  }

  MPI_Finalize();

  return 0;
}

3.11
求n个数和的表达式:x0+x1+…+x[n-1]
这n个数值的前缀和是n个部分和:x0, x0+x1, x0+x1+x2,…, x0+x1+…+x[n-1]
求前缀和事实上是求数值总和的一般化,它不只求出n个值的总和。
a. 设计一个串行算法来计算n个元素的前缀和。
b. 在有n个进程的系统和说那个并行化(a)小题中设计的串行程序,每个程序存储一个x_i值。
c. 设n=2^k, k为正整数。设计一个串行算法,然后将该算法并行化,使得这个并行算法仅需要k个通讯阶段。
d. MPI提供一个集合通信函数MPI_Scan,用来计算前缀和。该函数对有count个元素的数组进行操作,sendbuf_p和recvbuf_p都应该指向有count个datatype类型元素的数据块。op参数和MPI_Reduce中的op一样。编写一个MPI程序使每个MPI进程生成有count个元素的随机数数组,计算其前缀和并打印结果。

解答:
a

for (int i = 1; i < n; i++){
  a[i] += a[i - 1];
}

b

//如何将x值分发到每个线程上就不写了
//这里my_rank是进程号,假设my_rank号线程就是x[my_rank]
for (int i = 0; i < my_rank;i++){
  x_my_rank += x[i];
}

c
有k次的串行稍稍绕一点,开始以为是k次循环,后来是k个循环。
下面下一个n为8的情况

for(int i = 1; i < n; i += 2){
  x[i] += x[i - 1];
}

for(int i = 3; i < n; i += 4){
  x[i] += x[i - 2];
  x[i - 1] += x[i - 2];
}

for(int i = 7;i < n; i += 8){
  x[i] += x[i - 4];
  x[i - 1] += x[i - 4];
  x[i - 2] += x[i - 4];
  x[i - 3] += x[i - 4];
}

当然这里也可以用递归来写。

使用MPI的话,一个for循环算作1次通讯,这样就满足了题目的条件。

d.
使用MPI_Scan并不容易,困扰了好几天。
最后参考这里,并对原始代码进行修改,得到的如下代码。
这里和题目中不大一样的就是每一个进程都只有1个元素的随机数组。
这里暂时就不去纠结多个元素了。

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>

/* these variables are used by prefix sum also, hence here */

int main(int argc, char **argv)
{
  int i, num, psum, ksum, *arr, *input;
  int P, PID;

  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &PID);
  MPI_Comm_size(MPI_COMM_WORLD, &P);

  arr = (int*)malloc(P*sizeof(int));

  /* 0 creates input, sends */
  if (PID == 0) {
    srand(getpid());
    input = (int*)malloc(P*sizeof(int));
    printf("Input is:");
    for (i = 0; i < P; i++) {
      input[i] = rand()%10;
      printf(" %d", input[i]);
    }
    printf("\n");
  }

  MPI_Scatter(input, 1, MPI_INT, &num, 1, MPI_INT, 0, MPI_COMM_WORLD);

  /* prefix sum over all processes */
  MPI_Scan(&num, &psum, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
  printf("%d: Scan: %d\n", PID, psum);

  free(arr);

  MPI_Finalize();

  exit(0);

}
/* prefix sum */

3.12
可以用环形传递来替代蝶形结构的全归约,在环形传递结构体中,如果有p个线程,每个进程q向进程q+1发送数据(进程p-1向进程0发送数据)。这一过程持续循环直至所有进程都获得理想的结果。我们可以用以下代码实现全归约:
sum = temp_val = my_val;
for (i=1;i < p; i++) {
MPI_Sendrecv_replace(&temp_val, 1, MPI_INT, dest, sendtag, source, recvtag, comm, &status);
sum += temp_val;
}
a. 编写一个MPIc恒旭实现这一算法。与蝶形结构的全归约相比,它的性能如何?
b. 修改刚才的程序,实现前缀求和的运算。

解答:
A (1) 先来实现这个算法:(用c++实现)

#include "mpi.h"

#include <vector>
#include <array>
#include <iostream>

int loop_pass(int argc, char* argv[])
{
  int myid, numprocs, left, right;

  //MPI_Request request;
  MPI_Status status;

  std::vector<int> buffer{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

  MPI_Init(&argc, &argv);
  MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
  MPI_Comm_rank(MPI_COMM_WORLD, &myid);

  right = (myid + 1) % numprocs;
  left = myid - 1;
  if (left < 0) {
    left = numprocs - 1;
  }

  double start = MPI_Wtime();
  int temp = buffer.at(myid);

  for (int i = 1; i < numprocs; ++i) {
    MPI_Sendrecv_replace(
      &temp,
      1,
      MPI_INT,
      right, 1,
      left, 1,
      MPI_COMM_WORLD,
      &status);
    buffer[myid] += temp;
  }
  double end = MPI_Wtime();

  std::cout << "Thread ID[ " << myid << "] : " << buffer.at(myid) << std::endl;

  MPI_Finalize();

  if (myid == 0) {
    std::cout << "Runtime = " << end - start << std::endl;
  }

  return 0;
}

int main(int argc, char* argv[]) {
  loop_pass(argc, argv);
}

B

#include "mpi.h"

#include <vector>
#include <array>
#include <iostream>

int loop_pass(int argc, char* argv[])
{
  int myid, numprocs, left, right;

  //MPI_Request request;
  MPI_Status status;

  std::vector<int> buffer{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

  MPI_Init(&argc, &argv);
  MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
  MPI_Comm_rank(MPI_COMM_WORLD, &myid);

  right = (myid + 1) % numprocs;
  left = myid - 1;
  if (left < 0) {
    left = numprocs - 1;
  }

  double start = MPI_Wtime();
  int temp = buffer.at(myid);

  for (int i = 1; i < numprocs; ++i) {
    MPI_Sendrecv_replace(
      &temp,
      1,
      MPI_INT,
      right, 1,
      left, 1,
      MPI_COMM_WORLD,
      &status);
    if (myid >= i) {
      buffer[myid] += temp;
    }
  }


  double end = MPI_Wtime();

  std::cout << "Thread ID[ " << myid << "] : " << buffer.at(myid) << std::endl;

  MPI_Finalize();

  if (myid == 0) {
    std::cout << "Runtime = " << end - start << std::endl;
  }

  return 0;
}

int main(int argc, char* argv[]) {
  loop_pass(argc, argv);
}

(未完)

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值