C/C++与OpenMP
omp.h 是 OpenMP(Open Multi-Processing)的 C 和 C++ 接口。OpenMP 是一个支持多平台共享内存多处理编程的 API。它由几个主要硬件和软件供应商联合制定,包括 Sun, IBM, HP, Intel, SGI, AMD, Cray, Microsoft 等。OpenMP 是个并行编程模型,它支持 C,C++,和 Fortran 语言。
omp.h 头文件包含了用于指定并行区域(即需要并行执行的代码)的预处理指令、运行时库例程以及环境变量。例如,你可以使用 `#pragma omp parallel for` 为 for 循环启用并行执行。
这里是一个 OpenMP 的简单示例,它演示了如何使用 `omp.h` 头文件并行化一个 for 循环:
#include <omp.h>
#include <stdio.h>
int main() {
int i, n = 10;
int a[n], b[n], sum = 0;
// Initialize arrays a and b.
for (i = 0; i < n; i++) {
a[i] = i * 2;
b[i] = i * 3;
}
// Compute the dot product in parallel.
#pragma omp parallel for reduction(+:sum)
for (i = 0; i < n; i++) {
sum += a[i] * b[i];
}
printf("Sum = %d\n", sum);
return 0;
}
在这个例子中,#pragma omp parallel for reduction(+:sum)
这行代码指定了一个并行区域,它是一个 for 循环,并且指定了一个 reduction 操作,它是一个并行操作中常见的模式,用于合并每个线程的结果。注意到,并行化的代码需要满足数据依赖性的要求,以确保结果的正确性。
在运行使用 OpenMP 的程序时,你可以通过环境变量 OMP_NUM_THREADS
来设置线程数。例如,如果你想使用 4 个线程,你可以在运行程序之前设置 OMP_NUM_THREADS=4
。
Ubuntu运行OpenMP程序
在Ubuntu上运行上述代码,你需要安装一个支持OpenMP的C或C++编译器,例如GCC。GCC是一个广泛使用的开源编译器,对OpenMP有很好的支持。
要在Ubuntu上安装GCC,你可以在终端中输入以下命令:
sudo apt-get update
sudo apt-get install build-essential
build-essential
包包括了GCC和其他你可能需要的开发工具。
安装完GCC后,你可以用以下命令编译并行化的程序:
gcc -fopenmp program.c -o program
在这里,-fopenmp
是一个编译选项,告诉GCC你要使用OpenMP并行化代码。
然后,你可以运行编译好的程序:
./program
如果你想要设置运行程序时的线程数量,你可以设置环境变量 OMP_NUM_THREADS
:
export OMP_NUM_THREADS=4
./program
设置程序运行时使用的线程数量为4。实际运行时的线程数量可能会因系统负载等因素而调整。
omp_set_num_threads
omp_set_num_threads()
是一个 OpenMP 运行时库例程,用于设置并行区域启动时的线程数量。
当你在代码中调用 omp_set_num_threads(int num_threads)
函数时,你正在告诉 OpenMP 在下一个并行区域开始时,应该尝试使用 num_threads
个线程。这对于控制并行计算的粒度非常有用。如果不显式设置线程数,OpenMP 运行时将尝试使用尽可能多的线程,通常等于计算机中的物理或逻辑核心数。
需要注意的是,调用 omp_set_num_threads()
函数并不能保证一定会有指定数量的线程被使用。实际的线程数取决于系统的可用资源和 OpenMP 运行时的实现。另外,这个函数只影响调用它的线程后面创建的并行区域,对已经存在或并行执行的并行区域没有影响。
这是一个简单的例子,说明了 omp_set_num_threads()
的使用:
#include <omp.h>
#include <stdio.h>
int main() {
omp_set_num_threads(4);
#pragma omp parallel
{
printf("Hello, world! I'm thread %d\n", omp_get_thread_num());
}
return 0;
}
在这个程序中,omp_set_num_threads(4)
告诉 OpenMP 在接下来的并行区域使用 4 个线程。然后,每个线程都打印出一条消息,包括线程的编号,这个编号是通过 omp_get_thread_num()
函数获取的。
计算圆周率π值的代码
#include <stdio.h>
#include <omp.h>
static long num_steps = 1e9;
double step;
// NUM_THREADS 是并行执行时的线程数量
#define NUM_THREADS 1
// FS变量在这里起到的是避免伪共享(False Sharing)的作用。
// 伪共享是多线程并行计算中一个常见的性能问题,当两个线程访问同一个缓存行中的不同数据时,可能会导致性能下降。
// FS变量在这里保证了不同线程的数据位于不同的缓存行上,从而避免了伪共享。
#define FS 8
int main()
{
int i;
double x, pi, sum[NUM_THREADS*FS], ts, te;
step = 1.0/(double)num_steps;
// omp_set_num_threads() 是一个 OpenMP 运行时库例程,它用于设置并行区域启动时的线程数量;
// 调用该函数表示 OpenMP 在下一个并行区域开始时,应该尝试使用 num_threads 个线程。
omp_set_num_threads(NUM_THREADS);
// omp_get_wtime() 是一个 OpenMP 的运行时函数,用于获取当前的墙钟时间(Wall Clock Time),也就是真实经过的时间。
// 这个函数常常被用来在并行程序中测量代码段的执行时间。
ts = omp_get_wtime();
// #pragma omp parallel 是 OpenMP 中的一个编译指令,它标记了一个并行区域的开始。
// 这个指令会告诉 OpenMP 运行时系统,紧跟在这个指令后面的代码块应该在多个线程中并行执行。
// private(i) 是一个指定数据共享属性的子句。它告诉 OpenMP,变量 i 在每个线程中都应该有一个私有的副本。
// 每个线程都有它自己的 i,并且在每个线程中,i 的值都是独立的,不会被其他线程看到或修改。
// x 是在并行区域内定义的,所以它默认就是每个线程的私有变量。即在这个 #pragma omp parallel 区域内,每个线程都有自己的 x 变量。
#pragma omp parallel private(i)
{
double x;
int id;
id = omp_get_thread_num();
for ( i = id, sum[id*FS] = 0.0; i < num_steps; i += NUM_THREADS )
{
// 使用的是蒙特卡罗方法的变种,通过积分∫(4/(1+x²)) dx 从0到1来求解π
x = ( i+0.5 )*step;
sum[id*FS] += 4.0/(1.0+x*x);
}
}
for ( i = 0, pi = 0.0; i < NUM_THREADS; i++ )
{
pi += sum[i*FS]*step;
}
te = omp_get_wtime();
printf("pi = %.15f\ntime = %f s\n", pi, te - ts );
return 1;
}
伪共享问题
FS变量在这里起到的是避免伪共享(False Sharing)的作用。伪共享是多线程并行计算中一个常见的性能问题,当两个线程访问同一个缓存行中的不同数据时,可能会导致性能下降。
现代计算机中,处理器从内存中读取数据并不是一个个字节地读取,而是按照“缓存行”(通常为64字节)为单位进行读取。如果两个线程各自写入同一个缓存行中的不同位置,那么它们可能会互相干扰,导致需要多次读取同一个缓存行,这就是伪共享。
在你的代码中,sum
数组是线程间共享的,每个线程在sum[id*FS]
位置上累加自己的计算结果。由于可能存在伪共享,所以在sum
数组中为每个线程分配了FS(这里设为8)个double类型的空间,保证了不同线程的数据位于不同的缓存行上,从而避免了伪共享。
注意,尽管这个方法可以避免伪共享,但也可能导致内存使用效率下降,因为每个线程实际上只使用了分配给它的FS个double中的一个。在实际编程中,需要根据具体情况权衡内存使用和避免伪共享的需求。
omp_get_wtime
omp_get_wtime()
是一个 OpenMP 的运行时函数,用于获取当前的墙钟时间(Wall Clock Time),也就是真实经过的时间。这个函数常常被用来在并行程序中测量代码段的执行时间。
例如,你可能想要测量一段代码的运行时间
double start = omp_get_wtime();
// Code to be timed...
double end = omp_get_wtime();
double elapsed = end - start;
在上面的代码中,start
和 end
分别记录了代码段开始和结束时的时间,两者的差 elapsed
就是代码段的执行时间。
需要注意的是,omp_get_wtime()
返回的时间是以秒为单位的。此外,这个函数返回的是自某个固定点(通常是系统启动时)以来的时间,而这个固定点在不同的系统或运行时可能会不同。因此,omp_get_wtime()
通常只用于计算时间间隔,而不用于获取实际的日期或时间。
另外,omp_get_wtime()
返回的时间包括了所有的线程运行的时间,即使在多线程环境中,它也只返回墙钟时间,而不是 CPU 时间。也就是说,如果你在一个拥有 4 个线程的并行区域中使用 omp_get_wtime()
,它返回的时间并不会是 4 个线程的运行时间之和,而是实际经过的时间。
#pragma omp parallel
#pragma omp parallel
是 OpenMP 中的一个编译指令,它标记了一个并行区域的开始。这个指令会告诉 OpenMP 运行时系统,紧跟在这个指令后面的代码块应该在多个线程中并行执行。
private(i)
是一个指定数据共享属性的子句。它告诉 OpenMP,变量 i
在每个线程中都应该有一个私有的副本。每个线程都有它自己的 i
,并且在每个线程中,i
的值都是独立的,不会被其他线程看到或修改。
假设你有四个线程,每个线程都有一个 i
的副本,每个副本的初始值都是未定义的。当每个线程改变 i
的值时,只会影响该线程的 i
副本,而不会影响其他线程的 i
副本。这样可以防止线程间的数据竞争,确保并行程序的正确性。
假设你没有包含 private(i)
子句,那么 i
将默认是共享的,所有线程都将共享同一个 i
,修改 i
的值将可能引发数据竞争。
总的来说,#pragma omp parallel private(i)
开始了一个新的并行区域,并指定 i
在每个线程中都应该是私有的。
x
是在并行区域内定义的,所以它默认就是每个线程的私有变量。即在这个 #pragma omp parallel
区域内,每个线程都有自己的 x
变量。
这是因为在 C 和 C++ 中,变量的作用域是由它的位置决定的。在这种情况下,x
的作用域是它所在的代码块——在大括号 {}
中。每个线程都会执行这个代码块,所以每个线程都有它自己的 x
。
因此,即使你没有明确地把 x
声明为 private
,它仍然是私有的,每个线程都有它自己的副本。相比之下,i
是在并行区域外部定义的,所以如果你想让它在每个线程中都有自己的副本,就需要明确地声明为 private(i)
。