并行计算——基于MPI实现埃拉托斯特尼筛法及性能优化

具体代码可以在我码云里获得:https://gitee.com/wu_ji666/mpi_Eratosthenes.git

一.步骤及操作:

1.1, VS2017上MPI的安装和配置:

1.1.1安装

a.mpi官方下载地址:http://www.mpich.org/downloads/,下载后按照安装指导操作即可

1.1.2配置:

配置具体步骤(每次新建mpi工程时都要重新对工程进行配置):

右击项目-->>属性,进行配置: 

右上角-->>配置管理器-->>活动解决方案平台,选择:x86(我是64位系统,如果是32位,选择x64); 

VC++目录-->>包含目录,添加:“D:\Program Files (x86)\Microsoft SDKs\MPI\Include;” 

VC++目录-->>库目录,添加:“D:\Program Files (x86)\Microsoft SDKs\MPI\Lib\x64;”  

C/C++ -->> 预处理器-->>预处理器定义,添加:“MPICH_SKIP_MPICXX;” 

C/C++ -->> 代码生成 -->> 运行库,选择:多线程调试(/MTd); 

链接器 -->> 输入 -->> 附加依赖项,添加:“msmpi.lib;”

1.1.3调试:

 1 #include<stdio.h>
 2 
 3 #include<mpi.h>
 4 
 5 int main(int argc, char *argv[]) {
 6 
 7           int myid, numprocs;
 8 
 9     MPI_Init(&argc,&argv);
10 
11     MPI_Comm_rank(MPI_COMM_WORLD, &myid);
12 
13     MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
14 
15  
16 
17     printf("%d Hello world from process %d \n",numprocs, myid);
18 
19  
20 
21     MPI_Finalize();
22 
23     return 0;
24 
25 }

对这段代码编译并生成exe,用命令行执行mpiexec得到:

 

 图1.1.1

表明成功!

 

 

1.2,   埃拉托斯特尼筛法基准代码调试:

1.2.1.   关键代码

就是基准代码,做了一些修改:

a.加入#include <stdlib.h>

b.int改为long long int

c.加入结果写入文件模块(其他优化代码里一直沿用):

 1 void write_txtfile(double k, int num_pro, long long int num)
 2 
 3 {
 4 
 5     FILE * pFile = fopen("E:/VS_pro/文件名", "a+");
 6 
 7  
 8 
 9     if (NULL == pFile)
10 
11     {
12 
13          printf("error");
14 
15          return;
16 
17     }
18 
19     fprintf(pFile, "Thread num:%d,Arrange:%lld, Time:%10.6f\n", num_pro, num, k);
20 
21     fclose(pFile);
22 
23     return;
24 
25 }

 

1.3. 埃拉托斯特尼筛法去偶数优化:

1.3.1. 思想和方法:

   利用“大于2的质数都是奇数”这一知识,首先去掉所有偶数,偶数必然不是素数,这样相当于所需要筛选的数减少了一半,存储和计算性能都得到提高。 

1.3.2.   关键代码

 

int N = (n - 1) / 2;//just half of all

    low_value = (id * (N / p) + MIN(id, N % p)) * 2 + 3; //irst ele. in this pross.

    high_value = ((id + 1) * (N / p) + MIN(id + 1, N % p)) * 2 + 1;//last ele...

    size = (high_value - low_value) / 2 + 1;    //the size in this pross...

……

    // marked all

    for (int i = 0; i < size; i++) marked[i] = 0;

 

    if (!id) index = 0;

    prime = 3;

    do {

 

         if (prime * prime > low_value) {

             first = (prime * prime - low_value) / 2;

         }

         else {

 

             if (!(low_value % prime)) first = 0;

             //kicked out the even

             else if (low_value % prime % 2 == 0)

                  first = prime - ((low_value % prime) / 2);

             else first = (prime - (low_value % prime)) / 2;

         }

……

         if (!id) {

             while (marked[++index]);

             prime = index * 2 + 3; // start of 3

        }

 

事实上,去偶数重要的是索引的确定!

1.4.         埃拉托斯特尼筛法消除广播:

1.4.1     思想和方法

基准代码是通过进程0广播下一个筛选倍数的素数。进程之间需要通过MPI_Bcast函数进行通信。通信就一定会有开销,因此我们让每个进程都各自找出它们的前sqrt(n)个数中的素数,在通过这些素数筛选剩下的素数,这样一来进程之间就不需要每个循环广播素数了,性能得到提高。

1.4.2关键代码

 1 /*
 2 
 3  广播优化
 4 
 5     */
 6 
 7     //先找前sqrt(n)内的素数,再通过这些素数筛选后续素数
 8 
 9     int sqrt_N = (((int)sqrt((double)n)) - 1) / 2;
10 
11  
12 
13     low_NewArray_value = MIN(0, sqrt_N % p) * 2 + 3; //first ele. in this pross.(in NewArray)
14 
15     high_NewArray_value = ((sqrt_N / p) + MIN(1, sqrt_N % p)) * 2 + 1;//last ele...(in NewArray)
16 
17  
18 
19     // Bail out if all the primes used for sieving are not all held by process 0
20 
21     proc0_size = (sqrt_N - 1) / p;
22 
23  
24 
25     if ((2 + proc0_size) < (int)sqrt((double)sqrt_N)) {
26 
27          if (!id) printf("Too many processes \n");
28 
29          MPI_Finalize();
30 
31          exit(1);
32 
33     }
34 
35  
36 
37     NewMarked = (char *)malloc(sqrt_N);
38 
39     if (NewMarked == NULL) {
40 
41          printf("Cannot allocate enough memory \n");
42 
43          MPI_Finalize();
44 
45          exit(1);
46 
47     }
48 
49  
50 
51     //all ele. in NewArray was be marked
52 
53     for (int i = 0; i < sqrt_N; i++)
54 
55          NewMarked[i] = 0;
56 
57  
58 
59     index = 0;
60 
61  
62 
63     prime = 3;
64 
65     do {
66 
67          first = (prime * prime - low_NewArray_value) / 2;
68 
69  
70 
71          // 该素数的倍数排除
72 
73          for (int i = first; i < sqrt_N; i += prime) {
74 
75              NewMarked[i] = 1;
76 
77          }
78 
79  
80 
81          while (NewMarked[++index]);
82 
83          prime = index * 2 + 3;
84 
85  
86 
87     } while (prime * prime <= sqrt_N);
88 
89  

 

 

1.5.         埃拉托斯特尼筛法cache优化:

1.5.1.   思想和方法:

(0)Cache配置如图:

 

图1.5.1

(1)已知Cache是以CacheLine大小为单位交换数据的,考虑到CacheLine共用时的伪共享问题,首先进行Cacheline对齐,使每一个线程需要的数据在不同的Cacheline。我的电脑Cache配置已知CacheLine大小为64B,那么我们设置新的数据结构来完成数据Cacheline对齐:;并计算每个处理器占有的Cacheline数;

(2)考虑到Cache整体与内存的交换行为,我们进行分块。已知三级缓存为6M(之所以用三级缓存是为了避免L2\L1级缓存太小导致Cache命中率过低,反而降低优化性能)。在已知每个处理器占有的CacheLine数基础上,把这些CacheLIne集中成块。

列如:每个处理器可以获得的Cache大小为:Cache大小/核心数

占有CacheLIne数为:每个处理器可以获得的Cache大小/数据结构占有空间大小

1.5.2.   关键代码

代码注释详细,思路可见一斑。

  1 int Cache_linenum_pro = CACHE_SIZE / (CACHELINE_SIZE*p);//每个进程占有的cacheline
  2 
  3     int CacheBlock_size = Cache_linenum_pro * 8;//每个进程获得的用于存取long long的块大小
  4 
  5     int Block_N = CacheBlock_size - 1;
  6 
  7     int line_need = size / CacheBlock_size;//每个进程一共需要多少块
  8 
  9     int line_rest = size % CacheBlock_size;//多出来的cacheline
 10 
 11     int time_UseCache = 0;
 12 
 13  
 14 
 15  
 16 
 17     // allocate this process 's share of the array
 18 
 19     marked = (char *)malloc(CacheBlock_size);
 20 
 21     if (marked == NULL) {
 22 
 23          printf("Cannot allocate enough memory \n");
 24 
 25          MPI_Finalize();
 26 
 27          exit(1);
 28 
 29     }
 30 
 31  
 32 
 33     //就是块内去除偶数的判定过程
 34 
 35     count = 0;
 36 
 37  
 38 
 39     while (time_UseCache <= line_need) {
 40 
 41  
 42 
 43          //cache更新;
 44 
 45          Block_pos_last.value = (time_UseCache + 1) * Block_N + MIN(time_UseCache + 1, line_rest) - 1 + (id * (N / p) + MIN(id, N % p));
 46 
 47          Block_pos_first.value = time_UseCache * Block_N + MIN(time_UseCache, line_rest) + (id * (N / p) + MIN(id, N % p));
 48 
 49          Block_low_value.value = Block_pos_first.value * 2 + 3;
 50 
 51          if (time_UseCache == line_need) {
 52 
 53              Block_high_value.value = high_value;
 54 
 55              Block_pos_last.value = (id + 1) * (N / p) + MIN(id + 1, N % p) - 1;
 56 
 57              CacheBlock_size = ((Block_high_value.value - Block_low_value.value) >> 1) + 1;
 58 
 59          }
 60 
 61          else {
 62 
 63              Block_high_value.value = (Block_pos_last.value + 1) * 2 + 1;
 64 
 65          }
 66 
 67  
 68 
 69  
 70 
 71          index = 0;
 72 
 73          prime.value = 3;
 74 
 75          count_cacheBlock = 0;
 76 
 77  
 78 
 79          for (int i = 0; i < CacheBlock_size; i++) marked[i] = 0;
 80 
 81  
 82 
 83          // 块内素数
 84 
 85          do {
 86 
 87              if (prime.value * prime.value > Block_low_value.value) {
 88 
 89                   first.value = (prime.value * prime.value - Block_low_value.value) >> 1;
 90 
 91              }
 92 
 93              else {
 94 
 95                   if (!(Block_low_value.value % prime.value)) first.value = 0;
 96 
 97                   else if (Block_low_value.value % prime.value % 2 == 0) first.value = prime.value - ((Block_low_value.value % prime.value) >> 1);
 98 
 99                   else first.value = (prime.value - (Block_low_value.value % prime.value)) >> 1;
100 
101              }
102 
103              for (int i = first.value; i < CacheBlock_size; i += prime.value) {
104 
105                   marked[i] = 1;
106 
107              }
108 
109              while (NewMarked[++index]);
110 
111  
112 
113  
114 
115              prime.value = index * 2 + 3;
116 
117          } while (prime.value * prime.value <= Block_high_value.value);
118 
119  
120 
121  
122 
123          // 块内计数
124 
125          for (int i = 0; i < CacheBlock_size; i++) {
126 
127              if (marked[i] == 0) {
128 
129                   count_cacheBlock++;
130 
131              }
132 
133          }
134 
135  
136 
137          //all blockes togather
138 
139          count += count_cacheBlock;
140 
141          // 处理下一个块
142 
143          time_UseCache++;
144 
145     }
146 
147  

 

二、实验数据及结果分析:

2.1 实验结果

2.1.1.正确性展示(部分):

 

a.1e8:共5761455个素数;

1e8结果

b, 1e9:共50847534个素数;

 1e9结果

 

c, 1e10:共105097565个素数。

1e10结果 

2.1.2.结果评价:

a.1e8

执行时间:

图2.1.1

图2.1.1中可以看到在1-16个核心数范围内,所有程序的执行时间都下降明显,当核心数超过16后,执行时间逐渐平缓。

加速比图:

 

图2.1.2

图2.1.2(加速比图中)易于发现,去偶数与原基准程序成线性,而去广播和Cache优化则在核心数16左右达到做大,之后趋于平缓。

并行效率图:

 图2.1.3

图中显示了所有加速程序并行效率都随着核心数增加而下降;并且去偶数优化和基准程序基本一致,Cache优化并行效率同等核心数处于最高,去广播优化前期与基准程序一致,随着核心数增大,其并行效率明显高于基准程序。

纵比加速图:

同核心数下各优化方式相对于基准代码的加速效果:

 图2.1.4

可以看出去偶数相较于基准程序加速比接近2倍,去广播可接近4倍,Cache优化效果最好,在核心数16时达到最大加速倍数为10-11倍;其中去偶数优化优化效果可以看做常数2,去广播和Cache优化则成峰分布,在16核心数左右达到最大。

  

b. 在此将数据扩大到1e10:

执行时间:

图2.1.5

结果特征与1e8时一致。

加速比图:

 

图2.1.6

有趣的是,在1e8时加速比最大出现在16核心数,在1e10则为8;同时线性的特征消失(无论是基准程序还是去偶数)。

阿姆达尔效应验证:

 

 

 图2.1.7

这个就更有趣了,按照阿姆达尔效应,随着问题规模增加,同一并行算法的加速比应该上升,然而结果是1e8的加速比遥遥领先,而1e9和1e10则比较接近。

2.1.3. 结果分析:

(1).去偶数优化其实只是数量上线性的减少了问题规模(接近一半),通信和广播模式没有改变,所有优化效果是线性,接近2倍,并行效率也相似;

(2).核心数少时广播代价并不大,随着核心数增加广播代价增大,所以在核心数很大时去广播优化就明显优于去偶数。

(3).显然存在计算代价、通信代价(或者广播代价、计算代价、通信代价),而通信(通信和广播)会随着核心数增加增大,所以Cache优化和去广播优化在加速计算时,如果通信代价过大,计算加速带来的收益就下降,加速比曲线存在峰;而通信代价与直接计算代价的相对比例在不同问题规模不同,使不同问题规模下最大加速比对应核心数不同;

(4).通信代价存在,导致并行效率随核心数增大而下降;

(5).问题规模溢出本人计算机性能,出现大规模加速比下降,反阿姆达尔效应。

 

 

posted @ 2020-05-27 11:01  无极183  阅读(1267)  评论(0编辑  收藏  举报
/*
*/