OpenMP(仅供学习使用)

一、定义

OpenMP (Open Multi-Processing) 是一种用于并行编程的应用程序接口 (API),它针对共享内存多处理器系统的并行计算进行了优化。它是一个可移植、可伸缩的并行编程模型,可以在多个平台上运行,包括计算机集群和大型超级计算机。

OpenMP 是一个开放的标准,由一组 C、C++ 和 Fortran 编译指令组成,这些指令可以在编写串行代码的同时进行并行化,从而实现更高的程序性能。通过将代码分解为多个线程,OpenMP 可以使多个处理器同时处理问题,从而缩短了计算时间。

OpenMP 可以在不修改程序代码的情况下添加并行化,因为它使用编译器指令来控制线程的创建和同步。这使得它非常适合那些需要快速将现有代码并行化的应用程序。

OpenMP 提供了一系列指令,包括 #pragma 指令,用于告诉编译器哪些部分应该并行执行。在代码中使用这些指令可以实现并行计算,提高程序性能。

二、其加速的原理

OpenMP 的并行加速原理基于共享内存的并行计算模型。在共享内存计算机系统中,多个处理器可以同时访问共享的主内存。在 OpenMP 中,程序员可以通过编写特定的指令来控制如何将计算任务分配到不同的处理器上,以实现并行计算。

OpenMP 使用基于线程的并行计算模型。线程是程序执行流的基本单位,多个线程可以在同一时间访问共享的主内存,从而实现并行计算。程序员可以使用 OpenMP 指令来创建、同步和管理线程。OpenMP 还提供了一些指令,如 #pragma omp parallel 和 #pragma omp for,用于将代码块并行化。这些指令告诉编译器在运行时创建多个线程来执行指定的代码块,并通过同步机制确保线程之间的正确协调和数据共享。

通过将计算任务分配到多个线程上并行执行,OpenMP 可以利用计算机系统中的多个处理器,从而加速程序的执行。在运行时,OpenMP 将计算任务分配给不同的处理器,并使用同步机制确保线程之间正确地协调和共享数据。通过这种方式,OpenMP 可以实现并行加速,提高程序的性能。

三、代码应用例子

1.C++

假设我们有一个计算密集型的循环,需要对一个数组中的每个元素进行平方运算,代码如下:

#include <iostream>

const int N = 100000;

int main()
{
    double a[N];

    // 初始化数组
    for (int i = 0; i < N; i++) {
        a[i] = i;
    }

    // 计算数组每个元素的平方
    for (int i = 0; i < N; i++) {
        a[i] = a[i] * a[i];
    }

    return 0;
}

这段代码是串行执行的,如果数组大小很大,那么计算时间可能会比较长。我们可以使用 OpenMP 来并行化循环中的计算任务,从而加速计算过程。下面是使用 OpenMP 实现并行加速的代码:

#include <iostream>
#include <omp.h>

const int N = 100000;

int main()
{
    double a[N];

    // 初始化数组
    for (int i = 0; i < N; i++) {
        a[i] = i;
    }

    // 计算数组每个元素的平方
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        a[i] = a[i] * a[i];
    }

    return 0;
}

在这段代码中,我们使用了 #pragma omp parallel for 指令来将 for 循环并行化。该指令告诉编译器创建多个线程来执行循环中的计算任务,并使用同步机制确保线程之间的正确协调和数据共享。这样,我们就可以利用计算机系统中的多个处理器,加速计算过程。

2.Python

下面是一个简单的 Python 代码示例,用于计算斐波那契数列的第 n 项:

def fib(n):
    if n <= 1:
        return n
    else:
        return fib(n-1) + fib(n-2)

print(fib(40))

这段代码是串行执行的,如果 n 很大,计算时间可能会很长。我们可以使用 PyOpenMP 库来并行化计算任务,加速计算过程。下面是使用 PyOpenMP 实现并行加速的代码:

import pyopenmp

def fib(n):
    if n <= 1:
        return n
    else:
        with pyopenmp.Parallel() as p:
            if p.thread_num == 0:
                return fib(n-1) + fib(n-2)
            elif p.thread_num == 1:
                return fib(n-2)
            else:
                return fib(n-1)

print(fib(40))

在这段代码中,我们使用了 PyOpenMP 库中的 Parallel() 上下文管理器来创建多个线程来执行计算任务。在 Parallel() 上下文管理器中,程序员可以使用 if 语句来指定哪些线程执行哪些计算任务。在这个例子中,我们使用 if 语句来判断当前线程的编号,如果是第一个线程,则计算 fib(n-1) + fib(n-2);如果是第二个线程,则计算 fib(n-2);其他线程则返回 0。这样,我们就可以利用多个线程同时执行计算任务,从而加速计算过程。

需要注意的是,Python 是一种解释型语言,而 OpenMP 是为编译型语言设计的,并且 Python 的全局解释锁(GIL)可能会对多线程程序的执行效率造成影响。因此,在使用 PyOpenMP 时,需要仔细考虑线程数量和线程分配策略,以确保程序的正确性和高效性。

3.进阶例子

展示了如何使用 Numpy 和 PyOpenMP 库并行计算一个向量的元素和:

import numpy as np
import pyopenmp as omp

def parallel_sum(arr):
    n = len(arr)
    sum = 0
    # 开始并行计算
    with omp.Parallel(num_threads=4):
        # 获取当前线程编号
        tid = omp.get_thread_num()
        # 计算当前线程需要处理的部分
        start = tid * (n // omp.get_num_threads())
        end = (tid + 1) * (n // omp.get_num_threads()) if tid != omp.get_num_threads() - 1 else n
        # 在每个线程中计算部分和
        local_sum = 0
        for i in range(start, end):
            local_sum += arr[i]
        # 合并每个线程的部分和
        with omp.Lock():
            sum += local_sum
    return sum

arr = np.arange(10000000)
sum = parallel_sum(arr)
print("sum = ", sum)

在上面的代码中,我们首先使用 Numpy 库创建了一个长度为 10000000 的一维数组 arr,然后定义了一个函数 parallel_sum,用于计算该数组的元素和。在函数中,我们使用 with omp.Parallel(num_threads=4) 语句来开始并行计算,将任务分配给 4 个线程来完成。在每个线程中,我们首先获取当前线程的编号,然后计算出当前线程需要处理的部分,接着使用 for 循环来计算该部分的元素和。最后,我们使用 with omp.Lock(): 语句来将每个线程的部分和加入到总和中,确保计算的正确性。

需要注意的是,使用 PyOpenMP 库并行化代码需要谨慎处理数据的共享和同步,以免出现竞争条件和死锁等问题。此外,线程数的选择也需要考虑计算机的硬件资源和并行化的效果,需要进行仔细的调试和测试。

4.实战案例1

原代码:


	pcl::console::print_highlight("计算法线\n");
	pcl::NormalEstimationOMP<pcl::PointNormal, pcl::PointNormal> ne;
	ne.setInputCloud(cloud_input);
	ne.setKSearch(50);
	ne.compute(*cloud_input);

修改后代码:

#include <omp.h>

// ...

pcl::console::print_highlight("计算法线\n");
pcl::NormalEstimationOMP<pcl::PointNormal, pcl::PointNormal> ne;
ne.setInputCloud(cloud_input);
ne.setKSearch(50);

#pragma omp parallel for
for (int i = 0; i < cloud_input->size(); ++i)
{
    if (pcl::isFinite((*cloud_input)[i]))
    {
        pcl::PointNormal pn;
        ne.computePointNormal(*cloud_input, std::vector<int>{i}, pn.normal_x, pn.normal_y, pn.normal_z, pn.curvature);
        pn.x = (*cloud_input)[i].x;
        pn.y = (*cloud_input)[i].y;
        pn.z = (*cloud_input)[i].z;
        cloud_output->push_back(pn);
    }
}

在这段代码中,#pragma omp parallel for指定了一个并行循环,它会在多个线程上运行,并将工作分配给不同的线程。在循环中,我们只对点云中的有限点计算法线,并将结果存储在输出点云中。这样可以利用多核CPU的优势,提高计算效率。

5.实战案例2

1.点云降采样

#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/point_types.h>
#include <pcl/console/time.h> //控制台时间库
#include <omp.h> // OpenMP库

int main (int argc, char** argv)
{
    if (argc != 3)
    {
        std::cerr << "请提供一个输入PCD文件和一个输出PCD文件作为参数!" << std::endl;
        return -1;
    }

    pcl::console::TicToc time; // 创建计时器
    time.tic();

    // 读取点云数据
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_input (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::io::loadPCDFile (argv[1], *cloud_input);

    // 降采样
    pcl::VoxelGrid<pcl::PointXYZ> sor;
    sor.setInputCloud (cloud_input);
    sor.setLeafSize (0.01f, 0.01f, 0.01f);
    sor.setDownsampleAllData(true); // 防止数据集过大导致程序卡死
    sor.setSaveLeafLayout(true); // 为了能够精确测量程序执行时间
    sor.filter (*cloud_input);

    // 并行化降采样
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_output(new pcl::PointCloud<pcl::PointXYZ>);
    cloud_output->reserve(cloud_input->size()); // 预分配空间
    #pragma omp parallel for // 使用OpenMP并行化计算过程
    for(int i=0; i<cloud_input->size(); i++)
    {
        pcl::PointXYZ p = cloud_input->at(i);
        if(!std::isnan(p.x) && !std::isnan(p.y) && !std::isnan(p.z))
        {
            cloud_output->push_back(p);
        }
    }

    // 保存点云数据
    pcl::io::savePCDFileBinary (argv[2], *cloud_output);

    // 输出程序执行时间
    std::cout << "程序执行时间:" << time.toc() << "ms" << std::endl;

    return 0;
}

在上述代码中,我们使用VoxelGrid对输入点云数据进行降采样,然后使用OpenMP并行化降采样的过程。具体来说,我们使用了#pragma omp parallel for来实现并行化,这个指令可以让循环中的迭代操作并行执行。在本例中,我们将降采样后的点云数据分配给不同的线程进行处理,从而提高程序的执行效率。

2.点云降采样与平滑滤波

#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/filters/statistical_outlier_removal.h>
#include <pcl/console/time.h>
#include <pcl/point_cloud.h>
#include <pcl/point_representation.h>
#include <pcl/visualization/pcl_visualizer.h>

#include <iostream>
#include <omp.h>

using namespace std;
using namespace pcl;

typedef PointXYZ PointT;
typedef PointCloud<PointT> PointCloudT;

int main(int argc, char **argv)
{
    // Load point cloud data
    PointCloudT::Ptr cloud(new PointCloudT());
    if (io::loadPCDFile<PointT>("input.pcd", *cloud) == -1)
    {
        cerr << "Failed to load input point cloud!" << endl;
        return -1;
    }

    // Downsample the point cloud using voxel grid filter
    console::print_highlight("Downsampling the point cloud...\n");
    VoxelGrid<PointT> voxel_filter;
    voxel_filter.setInputCloud(cloud);
    voxel_filter.setLeafSize(0.01f, 0.01f, 0.01f);
    PointCloudT::Ptr cloud_downsampled(new PointCloudT());
    voxel_filter.filter(*cloud_downsampled);

    // Smooth the point cloud using statistical outlier removal filter
    console::print_highlight("Smoothing the point cloud...\n");
    StatisticalOutlierRemoval<PointT> outlier_filter;
    outlier_filter.setInputCloud(cloud_downsampled);
    outlier_filter.setMeanK(50);
    outlier_filter.setStddevMulThresh(1.0);
    PointCloudT::Ptr cloud_smoothed(new PointCloudT());
    outlier_filter.filter(*cloud_smoothed);

    // Visualize the original and processed point clouds
    visualization::PCLVisualizer viewer("Point Cloud Viewer");
    viewer.setBackgroundColor(0, 0, 0);
    viewer.addPointCloud<PointT>(cloud, "cloud");
    viewer.setPointCloudRenderingProperties(visualization::PCL_VISUALIZER_POINT_SIZE, 1, "cloud");
    viewer.addPointCloud<PointT>(cloud_smoothed, "cloud_smoothed");
    viewer.setPointCloudRenderingProperties(visualization::PCL_VISUALIZER_POINT_SIZE, 1, "cloud_smoothed");
    while (!viewer.wasStopped())
    {
        viewer.spinOnce(100);
    }

    return 0;
}

四、是否是只有含有for循环的代码才能用OpenMP来进行并行化操作

OpenMP 通常用于并行化循环结构中的迭代操作,但并不仅限于此。任何可以分解成多个可并行执行的任务的代码,都可以使用 OpenMP 进行并行化操作。比如,在矩阵乘法、图像处理、机器学习等领域中,都可以使用 OpenMP 进行并行化操作,加速计算过程。

然而,在进行并行化操作时,需要注意的是,如果任务之间存在依赖关系,那么就需要进行同步,避免出现竞争条件和死锁等问题。此外,线程数的选择也需要考虑计算机的硬件资源和并行化的效果,需要进行仔细的调试和测试。

总之,OpenMP 并不仅限于 for 循环结构,可以用于任何可以分解成多个可并行执行的任务的代码,但需要注意同步和线程数的选择。

五、存在的问题和注意事项

需要注意的是,如果点云中存在NaN或无穷大的值,会导致程序运行错误或结果不准确。因此,在进行计算前需要先对点云进行预处理,去除这些不合法的值。

此外,使用OpenMP并行化处理时,需要注意线程的数量和负载均衡。如果线程数量太多或太少,都会影响程序的效率。在代码实现中,可以通过设置OMP_NUM_THREADS环境变量来控制线程数量,并根据点云大小和处理任务的复杂度来调整线程数量和分配任务的方式,以达到最优的效率和负载均衡。

猜你喜欢

转载自blog.csdn.net/z377989129/article/details/129800139