并行算法设计

并行算法设计

并行算法设计

假定已有求解问题的串行算法,我们将 其改为并行版本

  • 并不一定是最好的策略——有些情况下,最 优并行算法与最优串行算法完全没有关系
  • 但很有用,我们很熟悉串行算法,很多时候 是切实可行的方法

并行算法与体系结构紧密相关!

任务分解、数据依赖、竞争条件

设计一个并行算法

  1. 计算任务的分解
    ❑ 如何将并行计算工作分解,交由众多进程/线程并发执行
  2. 保持依赖关系
    ❑ 计算结果与串行算法保持一致
  3. 额外开销
    ❑ 有多种类型的开销,要尽量降低

竞争条件与数据依赖

  1. 执行结果依赖于两个或更多事件的时序, 则存在竞争条件(race condition)
  2. 数据依赖(data dependence)就是两个内 存操作的序,为了保证结果的正确性,
    必须保持这个序
  3. 同步(synchronization)用来将多个线程 的执行串行化,或是将并发数据访问串
    行化

n 个数求和的例子

1.串行算法
sum = 0;
for (i = 0; i < n; i++) {
x = Compute next value(. . .);
sum += x; }
2. 并行算法
版本1:计算任务划分

假定每个核心计算连续n/t个元素的部分和(t为线程数或处理器数)

int block_length_per_thread = n/t;
int start = id * block_length_per_thread;
for (i=start; i<start+block_length_per_thread; i++) {
x = Compute_next_value(...);
sum += x; }

分析
1. 循环步之间的求和运算存在依赖→ 线程间依赖
❑ 但可以重排顺序,因为加法运算满足结合律
2. 取数-加法-存结果必须是原子操作,以保持结果与串行执行一致
3. 定义
原子性(atomicity):一组操作要么全部执行要么全不执行,则称其是原子的。即不会得到部分执行的结果。
互斥(mutual exclusion):任何时刻都只有一个线程在执行.

版本2:加锁

插入互斥(mutex),保证任何时刻只有一个线程读数-加法-存结果——原子操作

int block_length_per_thread = n/t;
mutex m;
int start = id * block_length_per_thread;
for (i=start; i<start+block_length_per_thread; i++) {
my_x = Compute_next_value(...); mutex_lock(m);
sum += my_x;
mutex_unlock(m);
}
版本3:粗粒度

在将局部和加到全局和时才加锁

int block_length_per_thread = n/t;
mutex m;
int my_sum;
int start = id * block_length_per_thread;
for (i=start; i<start+block_length_per_thread; i++) 
{
    my_x = Compute_next_value(...);
    my_sum += my_x; 
}
mutex_lock(m); 
sum += my_sum; 
mutex_unlock(m);
版本4:消除锁

“主“线程完成部分和相加

int block_length_per_thread = n/t;
mutex m;
shared my_sum[t];
int start = id * block_length_per_thread;
for (i=start; i<start+block_length_per_thread; i++) {
my_x = Compute_next_value(...);
my_sum[id] += my_x; }
if (id == 0) { // 主线程
sum = my_sum[0];
for (i=1; i<t; i++) sum += my_sum[i];
}
同步方法:障碍
  1. 如果主线程开始计算全局和的时候其他线程还未完成计算,就会得到不正确的结果
  2. 如何强制主线程等待其他线程完成之后再进行全局和计算呢?
  3. 定义
    障碍(barrier)阻塞线程继续执行,在此程序点等待,直到所有参与线程都到达障碍点才继续执行
版本5:消除锁,但增加障碍
int block_length_per_thread = n/t;
mutex m;
shared my_sum[t];
int start = id * block_length_per_thread;
for (i=start; i<start+block_length_per_thread; i++) {
my_x = Compute_next_value(...);
my_sum[t] += x; }
Synchronize_cores(); // 所有参与线程都设置障碍 
if (id == 0) { // 主线程
sum = my_sum[0];
for (i=1; i<t; i++) 
    sum += my_sum[t];
}
版本6:多核并行求全局和

这里写图片描述

求和例子总结
  • 求和计算有竞争条件和数据依赖
  • 使用mutex和障碍进行同步保证正确结果
  • 更多地进行本地运算,以提高线程间并行计算的粒度

  • 在这个例子中看到了哪些额外开销?
    ❑ 分配计算任务的额外代码
    ❑ 锁开销:加锁/解锁操作本身开销和线程间 竞争导致的空闲等待
    ❑ 负载不均

数据并行

如何设计并行算法

  1. 任务并行
    将求解问题的计算分解为任务,分配给多个核心
  2. 数据并行
    • 将求解问题涉及的数据划分给多个核心
    • 每个核心对不同数据进行相似的计算

其他任务划分方法

猜你喜欢

转载自blog.csdn.net/turing365/article/details/80300091