计算圆周率π值的OpenMP 实现

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;

在上面的代码中,startend 分别记录了代码段开始和结束时的时间,两者的差 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)

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LIHAORAN99

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值