什么是 OpenMP 中的 "implicit synchronization"
What is "implicit synchronization" in OpenMP
OpenMP 中的 "implicit synchronization" 到底是什么?您如何识别?我老师是这么说的
#pragma omp parallel
printf(“Hello 1\n”);
具有隐式同步。为什么?你怎么看?
同步是并行处理和 openmp 中的一个重要问题。通常并行处理是异步的。您知道有多个线程正在处理一个问题,但您无法确切知道它们的实际状态、它们正在处理的迭代等。同步允许您控制线程执行。
openmp 中有两种同步:显式和隐式。使用允许创建 barrier: #pragma omp barrier
的特定 openmp 构造完成显式同步。屏障是一种并行构造,只能由所有线程同时通过。因此,在屏障之后,您可以确切地知道所有线程的状态,更重要的是,您可以知道它们完成了多少工作。
隐式同步在两种情况下完成:
在平行区域的末尾。 Openmp 依赖于 fork-join 模型。程序启动时,会创建一个单线程(master thread)。当您通过 #pragma omp parallel
创建并行部分时,会创建多个线程 (fork)。这些线程将并发工作,并在并行部分结束时被销毁 (join)。所以在并行部分的末尾,你有一个同步并且你准确地知道所有线程的状态(他们已经完成了他们的工作)。这就是您给出的示例中发生的情况。并行部分仅包含 printf()
,最后,程序在继续之前等待所有线程终止。
在某些 openmp 构造 的末尾,如 #pragma omp for
或 #pragma omp sections
,存在隐式屏障。只要所有线程都没有达到屏障,任何线程都不能继续工作。这对于准确了解不同线程完成了哪些工作很重要。
例如,考虑以下代码。
#pragma omp parallel
{
#pragma omp for
for(int i=0; i<N; i++)
A[i]=f(i); // compute values for A
#pragma omp for
for(int j=0; j<N/2; j++)
B[j]=A[j]+A[j+N/2];// use the previously computed vector A
} // end of parallel section
由于所有线程都是异步工作的,因此您不知道哪些线程已经完成创建它们的 vector 部分 A
。如果没有同步,线程可能会快速完成第一个 for
循环的一部分,进入第二个 for
循环并访问向量 A
的元素,而线程应该计算还在第一个循环中,还没有计算出A[i]
.
对应的值
这就是 openmp 编译器添加隐式屏障来同步所有线程的原因。因此,您确定所有线程都已完成所有工作,并且当第二个 for
循环开始时 A
的所有值都已计算。
但在某些情况下,不需要同步。例如,考虑以下代码:
#pragma omp parallel
{
#pragma omp for
for(int i=0; i<N; i++)
A[i]=f(i); // compute values for A
#pragma omp for
for(int j=0; j<N/2; j++)
B[j]=g(j);// compute values for B
} // end of parallel section
显然这两个循环是完全独立的,如果 A
被正确计算以启动第二个 for
循环并不重要。所以同步对程序的正确性没有任何帮助
添加同步屏障有两个主要缺点:
如果函数 f()
的 运行 时间非常不同,您可能有一些线程已完成工作,而其他线程仍在计算。同步将迫使之前的线程等待,而这种闲置不会适当地利用并行性。
同步很昂贵。实现屏障的一种简单方法是在到达屏障时递增一个全局计数器,并等待计数器的值等于线程数omp_get_num_threads()
。为了避免线程之间的竞争,全局计数器的递增必须通过需要大量周期的原子读-修改-写来完成,并且等待计数器的正确值通常是通过自旋锁来完成的,这会浪费处理器周期。
所以有抑制隐式同步的构造,对前一个循环进行编程的最佳方法是:
#pragma omp parallel
{
#pragma omp for nowait // nowait suppresses implicit synchronisations.
for(int i=0; i<N; i++)
A[i]=f(i); // compute values for A
#pragma omp for
for(int j=0; j<N/2; j++)
B[j]=g(j);// compute values for B
} // end of parallel section
这样,一旦一个线程完成了第一个循环的工作,它将立即开始处理第二个 for
循环,并且,根据实际程序,这可能会显着减少执行时间。
OpenMP 中的 "implicit synchronization" 到底是什么?您如何识别?我老师是这么说的
#pragma omp parallel
printf(“Hello 1\n”);
具有隐式同步。为什么?你怎么看?
同步是并行处理和 openmp 中的一个重要问题。通常并行处理是异步的。您知道有多个线程正在处理一个问题,但您无法确切知道它们的实际状态、它们正在处理的迭代等。同步允许您控制线程执行。
openmp 中有两种同步:显式和隐式。使用允许创建 barrier: #pragma omp barrier
的特定 openmp 构造完成显式同步。屏障是一种并行构造,只能由所有线程同时通过。因此,在屏障之后,您可以确切地知道所有线程的状态,更重要的是,您可以知道它们完成了多少工作。
隐式同步在两种情况下完成:
在平行区域的末尾。 Openmp 依赖于 fork-join 模型。程序启动时,会创建一个单线程(master thread)。当您通过
#pragma omp parallel
创建并行部分时,会创建多个线程 (fork)。这些线程将并发工作,并在并行部分结束时被销毁 (join)。所以在并行部分的末尾,你有一个同步并且你准确地知道所有线程的状态(他们已经完成了他们的工作)。这就是您给出的示例中发生的情况。并行部分仅包含printf()
,最后,程序在继续之前等待所有线程终止。在某些 openmp 构造 的末尾,如
#pragma omp for
或#pragma omp sections
,存在隐式屏障。只要所有线程都没有达到屏障,任何线程都不能继续工作。这对于准确了解不同线程完成了哪些工作很重要。
例如,考虑以下代码。
#pragma omp parallel
{
#pragma omp for
for(int i=0; i<N; i++)
A[i]=f(i); // compute values for A
#pragma omp for
for(int j=0; j<N/2; j++)
B[j]=A[j]+A[j+N/2];// use the previously computed vector A
} // end of parallel section
由于所有线程都是异步工作的,因此您不知道哪些线程已经完成创建它们的 vector 部分 A
。如果没有同步,线程可能会快速完成第一个 for
循环的一部分,进入第二个 for
循环并访问向量 A
的元素,而线程应该计算还在第一个循环中,还没有计算出A[i]
.
这就是 openmp 编译器添加隐式屏障来同步所有线程的原因。因此,您确定所有线程都已完成所有工作,并且当第二个 for
循环开始时 A
的所有值都已计算。
但在某些情况下,不需要同步。例如,考虑以下代码:
#pragma omp parallel
{
#pragma omp for
for(int i=0; i<N; i++)
A[i]=f(i); // compute values for A
#pragma omp for
for(int j=0; j<N/2; j++)
B[j]=g(j);// compute values for B
} // end of parallel section
显然这两个循环是完全独立的,如果 A
被正确计算以启动第二个 for
循环并不重要。所以同步对程序的正确性没有任何帮助
添加同步屏障有两个主要缺点:
如果函数
f()
的 运行 时间非常不同,您可能有一些线程已完成工作,而其他线程仍在计算。同步将迫使之前的线程等待,而这种闲置不会适当地利用并行性。同步很昂贵。实现屏障的一种简单方法是在到达屏障时递增一个全局计数器,并等待计数器的值等于线程数
omp_get_num_threads()
。为了避免线程之间的竞争,全局计数器的递增必须通过需要大量周期的原子读-修改-写来完成,并且等待计数器的正确值通常是通过自旋锁来完成的,这会浪费处理器周期。
所以有抑制隐式同步的构造,对前一个循环进行编程的最佳方法是:
#pragma omp parallel
{
#pragma omp for nowait // nowait suppresses implicit synchronisations.
for(int i=0; i<N; i++)
A[i]=f(i); // compute values for A
#pragma omp for
for(int j=0; j<N/2; j++)
B[j]=g(j);// compute values for B
} // end of parallel section
这样,一旦一个线程完成了第一个循环的工作,它将立即开始处理第二个 for
循环,并且,根据实际程序,这可能会显着减少执行时间。