【016】还在为并行编程头疼?CMake 一键解决!

一、引言

高性能计算为了充分发挥现代计算机硬件(特别是多核处理器和分布式计算集群)的性能潜力,并行编程就非常重要。OpenMP 和 MPI 是两种常见的并行编程模型,分别适合共享内存和分布式内存系统。OpenMP 可以通过简单的编译指示(pragma)把串行代码并行化,而 MPI 则通过消息传递机制实现进程间的通信和协作。

CMake 是一个强大的跨平台构建系统生成工具,能够简化项目的配置、编译和链接过程。对于涉及 OpenMP 和 MPI 的并行程序,正确配置 CMake 很重要,要确保代码能够正确地编译、链接并执行。

这篇文章就详细介绍如何在 CMake 中检测和配置 OpenMP 和 MPI 并行环境。通过具体的示例代码和 CMakeLists.txt 配置文件,演示如何使用 CMake 的现代特性(特别是导入目标)来简化 OpenMP 和 MPI 的使用。同时,也提供兼容旧版 CMake 的方法,确保在不同 CMake 版本上的可移植性。本文旨在帮助理解 CMake 中 OpenMP 和 MPI 的配置原理,并提供最佳实践指南,从而提高并行程序的开发效率。

二、配置 OpenMP 并行环境

在当今的计算环境中,多核处理器已成为标准配置。为了充分利用这些硬件资源并提升程序的执行效率,尤其对于性能敏感的应用程序,采用并行编程模型至关重要。OpenMP (Open Multi-Processing) 是一个广泛使用的应用程序接口(API),它支持共享内存多处理器编程,是实现并行计算的标准之一。

OpenMP 的一个显著优势在于,它允许程序员通过添加预处理指令(也称为编译指示或 Pragma)来并行化现有代码,而无需对程序结构进行大规模修改或完全重写。一旦使用分析工具确定了代码中的性能瓶颈区域,程序员就可以在这些区域前插入 OpenMP 指令,指示编译器生成可并行执行的代码,从而在多核处理器上获得性能收益。

这里将详细展示如何在 CMake 构建系统中,检测并启用 OpenMP 支持,从而编译和链接包含 OpenMP 指令的程序。使用一个简单的 C++ 程序作为示例,该程序利用 OpenMP 进行并行计算。对于相对较新的 CMake 版本(特别是 3.9 或更高版本),CMake 对 OpenMP 提供了非常良好且简化的支持。

注意: 并非所有编译器都默认支持 OpenMP。例如,在某些 Linux 发行版上,Clang 编译器的默认版本可能不包含 OpenMP 支持。为了在 macOS 上使用 OpenMP,可能需要安装非 Apple 版本的 Clang(例如,通过 Homebrew 或 Conda 安装的 Clang),或者使用 GNU 编译器(GCC)。此外,如果使用 Clang,可能还需要单独安装 libomp 库(例如,通过 brew install libomp 或从 https://iscinumpy.gitlab.io/post/omp-on-high-sierra/ 等资源获取)。

2.1、准备工作

要使 C 或 C++ 程序能够使用 OpenMP 功能,要包含 omp.h 头文件,并在编译时启用 OpenMP 编译选项,链接到相应的 OpenMP 运行时库。编译器会识别代码中的 OpenMP 编译指示,并据此生成并行代码。

在本示例中,构建以下 C++ 源代码 (example.cpp)。这段代码计算从 1 到 N 的整数和,其中 N 作为命令行参数传入。它利用 OpenMP 的 parallel for 指令并行化求和循环,并使用 reduction 子句安全地累加结果。

// example.cpp
#include <iostream> 
#include <string>
#include <omp.h>    // 包含 OpenMP API 头文件

int main(int argc, char *argv[])
{
    // 检查命令行参数数量
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " <number_N>\n";
        return 1;
    }

    // 打印可用的处理器核心数量
    std::cout << "Number of available processors: " << omp_get_num_procs()
                << std::endl;
    // 打印当前程序可以使用的最大线程数(通常由 OMP_NUM_THREADS 环境变量控制,否则为系统默认)
    std::cout << "Number of threads: " << omp_get_max_threads() << std::endl;

    // 将命令行参数转换为长整型 N
    long long n = 0;
    try {
        n = std::stoll(argv[1]); // 使用 std::stoll 确保支持大数
    } catch (const std::out_of_range& oor) {
        std::cerr << "Error: Number N is too large or too small.\n";
        return 1;
    } catch (const std::invalid_argument& ia) {
        std::cerr << "Error: Invalid number N provided.\n";
        return 1;
    }


    std::cout << "We will form sum of numbers from 1 to " << n << std::endl;

    // 启动计时器,获取当前墙钟时间
    auto t0 = omp_get_wtime();

    // 初始化求和变量
    long long s = 0LL; // 使用 long long 以避免溢出大数和

    // OpenMP 并行区域:将 for 循环并行化
    // #pragma omp parallel for: 指示编译器将紧随其后的 for 循环并行执行
    // reduction(+:s): 指定对变量 s 执行归约操作。每个线程计算其部分的和,最后将所有线程的部分和累加到 s。
    #pragma omp parallel for reduction(+ : s)
    // 循环变量也使用 long long
    for (long long i = 1; i <= n; ++i) {
        s += i;
    }

    // 停止计时器
    auto t1 = omp_get_wtime();

    // 打印结果和耗时
    std::cout << "Sum: " << s << std::endl;
    std::cout << "Elapsed wall clock time: " << t1 - t0 << " seconds" << std::endl;

    return 0;
}

这段代码的核心是 omp.h 头文件和 #pragma omp parallel for reduction(+ : s) 指令。

2.2、具体步骤

CMakeLists.txt 文件中配置 OpenMP 支持的步骤:

  1. 首先指定项目所需的最低 CMake 版本。对于 OpenMP 的现代支持(通过导入目标),建议使用 CMake 3.9 或更高版本。然后声明项目名称和使用的编程语言(C++)。

    # 设置项目所需的最低 CMake 版本。建议 3.9 或更高以获得 OpenMP 的现代支持。
    cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
    
    # 定义项目名称,指定使用的语言为 C++
    project(OpenMP_Example LANGUAGES CXX)
    
  2. 为了确保代码的可移植性和使用现代 C++ 特性,建议显式设置 C++ 标准。

    # 强制使用 C++11 标准。示例代码使用了 C++11 特性 (如 auto, std::stoll)。
    set(CMAKE_CXX_STANDARD 11)
    # 禁止使用编译器扩展,确保符合标准
    set(CMAKE_CXX_EXTENSIONS OFF)
    # 如果无法满足标准要求,则配置失败
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    
  3. CMake 提供了一个内置模块 FindOpenMP.cmake 来检测系统上的 OpenMP 支持。通过调用 find_package(OpenMP REQUIRED),CMake 会尝试找到 OpenMP 编译器支持和运行时库。

    # 查找 OpenMP 支持。REQUIRED 关键字表示 OpenMP 是必需的,如果找不到则 CMake 配置失败。
    find_package(OpenMP REQUIRED)
    
    # 打印找到的 OpenMP 信息,方便调试
    if(OpenMP_FOUND)
        message(STATUS "Found OpenMP: ${OpenMP_CXX_FLAGS}")
        # OpenMP_CXX_FLAGS 包含了编译器启用 OpenMP 所需的标志 (例如 -fopenmp, /openmp)
    else()
        message(FATAL_ERROR "OpenMP support not found. Please ensure your compiler supports OpenMP and it's properly configured.")
    endif()
    
  4. 定义可执行目标,并将源代码文件 (example.cpp) 添加进去。最关键的一步是链接到 OpenMP 库。在 CMake 3.9 及更高版本中,FindOpenMP 模块会提供一个名为 OpenMP::OpenMP_CXX导入目标 (IMPORTED Target)。这个导入目标封装了所有编译和链接 OpenMP 所需的标志、包含目录和库路径。

    # 添加一个可执行目标,使用我们的 C++ 源文件
    add_executable(example example.cpp)
    
    # 将可执行目标链接到 OpenMP 库。
    # PUBLIC 关键字表示这些链接信息不仅用于当前目标,也会传递给依赖于此目标的其他目标。
    # OpenMP::OpenMP_CXX 是 FindOpenMP 模块提供的导入目标,它包含了所有必要的编译和链接信息。
    target_link_libraries(example
      PUBLIC
          OpenMP::OpenMP_CXX
    )
    

完整的 CMakeLists.txt 文件:

# 设置项目所需的最低 CMake 版本。建议 3.9 或更高以获得 OpenMP 的现代支持。
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)

# 定义项目名称,指定使用的语言为 C++
project(OpenMP_Example LANGUAGES CXX)

# 强制使用 C++11 标准,并禁止编译器扩展,确保符合标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 查找 OpenMP 支持。REQUIRED 关键字表示 OpenMP 是必需的。
find_package(OpenMP REQUIRED)

# 打印找到的 OpenMP 信息,方便调试
if(OpenMP_FOUND)
    message(STATUS "Found OpenMP: ${OpenMP_CXX_FLAGS}")
else()
    message(FATAL_ERROR "OpenMP support not found. Please ensure your compiler supports OpenMP and it's properly configured.")
endif()

# 添加一个可执行目标,使用我们的 C++ 源文件
add_executable(example example.cpp)

# 将可执行目标链接到 OpenMP 库。
# OpenMP::OpenMP_CXX 是 FindOpenMP 模块提供的导入目标,它包含了所有必要的编译和链接信息。
target_link_libraries(example
  PUBLIC
      OpenMP::OpenMP_CXX
)

配置、构建并运行这个 CMake 项目:

(1)创建构建目录并进入,运行 CMake 进行配置:

mkdir build
cd build
cmake ..

如果 CMake 成功找到 OpenMP 支持,将看到类似以下输出:

-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found OpenMP_CXX: -fopenmp (found version "4.5") 
-- Found OpenMP: TRUE (found version "4.5")  
-- Found OpenMP: -fopenmp
-- Configuring done
-- Generating done

(2)执行构建,运行可执行文件进行并行测试:

cmake --build .
./example 1000000000

将看到类似以下输出(具体数字取决于你的 CPU 核心数和性能):

Number of available threads: 16
Number of threads: 16
we will calculate the sum of 1 to 1000000000
sum = 500000000500000000
Elapsed wall clock time: 0.159563 seconds

为了比较,可以通过设置 OMP_NUM_THREADS 环境变量来限制 OpenMP 使用的线程数,例如将其设置为 1,模拟串行执行:

env OMP_NUM_THREADS=1 ./example 1000000000

输出将显示 Number of threads: 1,并且 Elapsed wall clock time 会显著增加,例如:

Number of available threads: 16
Number of threads: 1
we will calculate the sum of 1 to 1000000000
sum = 500000000500000000
Elapsed wall clock time: 0.94243 seconds

通过比较两次运行的耗时,可以直观地看到 OpenMP 并行化带来的性能提升。

2.3、工作原理深度解析

  1. find_package(OpenMP REQUIRED) 这个命令调用 CMake 内置的 FindOpenMP.cmake 模块。该模块负责检测系统上 C、C++ 编译器对 OpenMP 的支持。它会检查编译器是否支持 OpenMP 标志(例如 GCC/Clang 的 -fopenmp,MSVC 的 /openmp),并尝试编译和链接一个小的测试程序来验证 OpenMP 功能是否可用。如果成功找到,它会设置 OpenMP_FOUND 为真,并设置一些变量,例如 OpenMP_CXX_FLAGS 包含 C++ 编译器启用 OpenMP 所需的编译标志。

  2. 导入目标 (OpenMP::OpenMP_CXX): 这是 CMake 3.9 及更高版本中处理外部库依赖的现代化和推荐方式。当 find_package(OpenMP) 成功时,它会创建一个或多个导入目标。对于 C++,这个导入目标是 OpenMP::OpenMP_CXX。导入目标是一个“伪目标”,它并不代表你的项目中的源代码,而是代表一个外部已经存在或将在构建时可用的库。这个导入目标封装了所有与 OpenMP 相关的接口属性 (Interface Properties),例如:

    • INTERFACE_COMPILE_OPTIONS:包含启用 OpenMP 所需的编译器标志(例如 -fopenmp)。
    • INTERFACE_INCLUDE_DIRECTORIES:如果 OpenMP 需要特定的头文件路径,会包含在这里。
    • INTERFACE_LINK_LIBRARIES:包含 OpenMP 运行时库的链接信息。
      通过使用 target_link_libraries(example PUBLIC OpenMP::OpenMP_CXX),我们告诉 CMake:
    • example 目标需要链接 OpenMP::OpenMP_CXX 所代表的 OpenMP 库。
    • PUBLIC 关键字表示 OpenMP::OpenMP_CXX 的所有接口属性(编译选项、包含目录、链接库)不仅会应用于 example 目标本身的编译和链接,也会传递给任何直接或间接依赖 example 目标的其他目标。这意味着,如果另一个目标 my_app 依赖于 example,那么 my_app 也会自动继承 OpenMP 的编译和链接要求。这大大简化了大型项目的依赖管理。
  3. 这种方法极大地简化了 CMakeLists.txt。不再需要手动获取 OpenMP_CXX_FLAGS 并将其传递给 target_compile_optionstarget_link_libraries。所有的复杂性都被封装在 OpenMP::OpenMP_CXX 导入目标中,CMake 会自动处理正确的编译标志和链接库。这使得构建脚本更简洁、更健壮,并减少了因手动管理编译器特定标志而导致的错误。

  4. 如果想查看导入目标 OpenMP::OpenMP_CXX 到底包含了哪些属性,可以使用 CMakePrintHelpers 模块提供的 cmake_print_properties 命令(仅用于调试目的):

    # 包含 CMakePrintHelpers 模块
    include(CMakePrintHelpers)
    
    # 打印 OpenMP::OpenMP_CXX 导入目标的接口属性
    cmake_print_properties(
        TARGETS
            OpenMP::OpenMP_CXX
        PROPERTIES
            INTERFACE_COMPILE_OPTIONS
            INTERFACE_INCLUDE_DIRECTORIES
            INTERFACE_LINK_LIBRARIES
            # 也可以打印其他属性,例如 LOCATION
        )
    

    运行 CMake 后,你会在配置输出中看到这些属性的值,例如 INTERFACE_COMPILE_OPTIONS 可能会显示 -fopenmp

2.4、兼容旧版 CMake

对于低于 CMake 3.9 的版本,FindOpenMP 模块可能不会提供导入目标。在这种情况下,需要手动将 FindOpenMP 模块设置的变量应用到目标上。这涉及使用 OpenMP_CXX_FLAGS 变量:

# 仅适用于 CMake < 3.9 的旧方法
# 查找 OpenMP (在旧版本中可能不提供导入目标)
find_package(OpenMP REQUIRED)

add_executable(example example.cpp)

# 手动添加编译器选项以启用 OpenMP
target_compile_options(example
  PUBLIC
      ${OpenMP_CXX_FLAGS}
  )

# 在某些旧版本 CMake 或特定编译器上,可能还需要手动设置链接标志
# set_target_properties(example
#   PROPERTIES
#       LINK_FLAGS ${OpenMP_CXX_FLAGS}
#   )

# 链接 OpenMP 库 (旧版本通常通过 OpenMP_CXX_LIBRARIES 提供)
target_link_libraries(example
  PUBLIC
      ${OpenMP_CXX_LIBRARIES} # 或者直接使用 ${OpenMP_LIBRARIES}
  )

强烈建议升级到 CMake 3.9 或更高版本,并采用现代化的导入目标方式,因为它更加简洁、健壮,并且更好地处理了跨编译器和平台差异。

通过正确配置 CMake 来检测和链接 OpenMP,可以轻松地在 C/C++ 项目中启用并行计算,从而充分利用多核处理器的性能潜力。

三、配置 MPI 并行环境

在高性能计算 (HPC) 领域,并行编程是利用多核处理器和分布式系统强大计算能力的关键。消息传递接口 (Message Passing Interface, MPI) 是分布式内存并行编程的实际标准,它允许程序在多个独立的计算节点(或同一节点上的多个进程)之间通过明确的消息传递进行通信和协作。

MPI 可以看作是 OpenMP(共享内存并行方式)的强大补充。虽然最新的 MPI 实现也可能支持共享内存优化,但其核心优势在于处理分布式内存系统,即每个处理器拥有独立的内存空间。在高性能计算中,一种常见的混合编程模型是结合使用 MPI 和 OpenMP:MPI 用于在不同计算节点之间进行通信,而 OpenMP 则用于在每个节点内部的共享内存处理器上实现并行化。

一个典型的 MPI 实现通常包括以下几个核心组件:

  1. 运行时库: 提供 MPI 函数的实际实现,供应用程序调用。
  2. 头文件: 包含 MPI 函数的声明和常量的定义,供 C/C++ 和 Fortran 编译器使用。
  3. 编译器包装器: 这是 MPI 环境的一个重要特征。它们是特殊的脚本或程序,用于封装底层的 C/C++ 或 Fortran 编译器。例如,mpicxx (或 mpiCC/mpic++) 用于 C++,mpicc 用于 C,mpifort 用于 Fortran。这些包装器会自动添加编译和链接 MPI 程序所需的额外编译器标志、头文件路径和库路径,极大地简化了 MPI 程序的编译过程。
  4. 启动器: 用于启动 MPI 程序的并行执行。这些工具(如 mpirunmpiexecorterun)负责在指定的进程数量和节点上启动程序的多个副本,并建立它们之间的通信基础设施。

本节将详细展示如何在 CMake 构建系统中找到合适的 MPI 实现,并编译一个简单的 “Hello, World” MPI 程序。

3.1、准备工作

使用经典的 “Hello, World” MPI 示例程序 (helloMPI.cpp)。这个程序会初始化 MPI 环境,获取当前进程在通信组中的排名 (rank) 和通信组的总大小 (size),以及当前进程运行的处理器名称,然后打印一条消息。

// helloMPI.cpp
#include <iostream>
#include <mpi.h>    // 包含 MPI 库的头文件

int main(int argc, char **argv)
{
  // 1. 初始化 MPI 环境
  // MPI_Init 必须是任何 MPI 调用之前被调用的第一个函数。
  // argc 和 argv 参数通常不被 MPI 实现直接使用,但为了兼容性保留。
  MPI_Init(&argc, &argv); // 推荐传递 argc 和 argv

  // 2. 获取通信组(通常是 MPI_COMM_WORLD)中的进程总数
  int world_size;
  // MPI_Comm_size 函数用于获取给定通信器中的进程数量。
  // MPI_COMM_WORLD 是一个预定义的通信器,包含所有启动的 MPI 进程。
  MPI_Comm_size(MPI_COMM_WORLD, &world_size);

  // 3. 获取当前进程在通信组中的排名 (rank)
  int world_rank;
  // MPI_Comm_rank 函数用于获取当前进程在给定通信器中的唯一排名。
  // 排名从 0 到 world_size - 1。
  MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);

  // 4. 获取当前进程运行的处理器名称
  char processor_name[MPI_MAX_PROCESSOR_NAME]; // 定义一个足够大的字符数组来存储处理器名称
  int name_len;                               // 存储处理器名称的实际长度
  // MPI_Get_processor_name 函数用于获取当前进程所运行的物理处理器的名称。
  MPI_Get_processor_name(processor_name, &name_len);

  // 5. 打印 "Hello World" 消息
  // 每个 MPI 进程都会执行这段代码,并打印出自己的信息。
  std::cout << "Hello world from processor " << processor_name
            << ", rank " << world_rank
            << " out of " << world_size << " processors" << std::endl;

  // 6. 结束 MPI 环境
  // MPI_Finalize 必须是任何 MPI 调用之后被调用的最后一个函数。
  // 调用后,不能再进行任何 MPI 调用。
  MPI_Finalize();

  return 0;
}

这个程序展示了 MPI 编程的基本流程:初始化、获取进程信息、执行并行任务(这里是打印),以及最终的清理。

3.2、具体步骤

在 CMake 中查找和配置 MPI 环境,将使用标准模块 FindMPI.cmake

  1. 首先,定义项目所需的最低 CMake 版本。对于 MPI 的现代支持(通过导入目标),建议使用 CMake 3.9 或更高版本。然后声明项目名称和使用的编程语言(C++)。

    # 设置项目所需的最低 CMake 版本。建议 3.9 或更高以获得 MPI 的现代支持。
    cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
    
    # 定义项目名称,指定使用的语言为 C++
    project(MPI_Example LANGUAGES CXX)
    
    # 强制使用 C++11 标准。示例代码使用了 C++11 特性。
    set(CMAKE_CXX_STANDARD 11)
    # 禁止使用编译器扩展,确保符合标准
    set(CMAKE_CXX_EXTENSIONS OFF)
    # 如果无法满足标准要求,则配置失败
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    
  2. CMake 提供了 FindMPI.cmake 模块来检测系统上的 MPI 实现。调用 find_package(MPI REQUIRED) 会尝试查找 MPI 的库、头文件以及编译器包装器。

    # 查找 MPI 支持。REQUIRED 关键字表示 MPI 是必需的,如果找不到则 CMake 配置失败。
    # FindMPI 模块会尝试找到 C、C++ 和 Fortran 的 MPI 实现。
    find_package(MPI REQUIRED)
    
    # 打印找到的 MPI 信息,方便调试
    if(MPI_FOUND)
        message(STATUS "Successfully found MPI:")
        message(STATUS "  MPI Version: ${MPI_VERSION}")
        if(MPI_CXX_FOUND)
            message(STATUS "  C++ MPI Compiler: ${MPI_CXX_COMPILER}")
            message(STATUS "  C++ MPI Includes: ${MPI_CXX_INCLUDE_PATH}")
            message(STATUS "  C++ MPI Libraries: ${MPI_CXX_LIBRARIES}")
        else()
            message(WARNING "C++ MPI support not found, but general MPI was found. This might indicate an issue if you plan to use C++.")
        endif()
        # 也可以打印 MPI 启动器,例如 MPI_EXECUTABLE
        if(MPI_EXECUTABLE)
            message(STATUS "  MPI Launcher: ${MPI_EXECUTABLE}")
        endif()
    else()
        message(FATAL_ERROR "MPI support not found. Please ensure an MPI implementation is installed and configured.")
    endif()
    

    FindMPI 模块会设置一系列变量,例如 MPI_FOUND(是否找到 MPI)、MPI_VERSION(找到的 MPI 版本)、MPI_CXX_FOUND(是否找到 C++ MPI 支持)、MPI_CXX_COMPILER(C++ MPI 包装器路径)、MPI_CXX_INCLUDE_PATH(C++ MPI 头文件路径)和 MPI_CXX_LIBRARIES(C++ MPI 库路径)。

  3. 定义可执行目标,并将源代码文件 (helloMPI.cpp) 添加进去。与 OpenMP 类似,CMake 3.9 及更高版本的 FindMPI 模块会提供一个名为 MPI::MPI_CXX导入目标 (IMPORTED Target)。这个导入目标封装了所有编译和链接 MPI C++ 程序所需的标志、包含目录和库路径。

    # 添加一个可执行目标,使用我们的 C++ 源文件
    add_executable(helloMPI helloMPI.cpp)
    
    # 将可执行目标链接到 MPI 库。
    # PUBLIC 关键字表示这些链接信息不仅用于当前目标,也会传递给依赖于此目标的其他目标。
    # MPI::MPI_CXX 是 FindMPI 模块提供的导入目标,它包含了所有必要的编译和链接信息。
    target_link_libraries(helloMPI
      PUBLIC
          MPI::MPI_CXX
    )
    

完整的 CMakeLists.txt 文件如下:

# 设置项目所需的最低 CMake 版本。建议 3.9 或更高以获得 MPI 的现代支持。
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)

# 定义项目名称,指定使用的语言为 C++
project(MPI_Example LANGUAGES CXX)

# 强制使用 C++11 标准,并禁止编译器扩展,确保符合标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 查找 MPI 支持。REQUIRED 关键字表示 MPI 是必需的。
find_package(MPI REQUIRED)

# 打印找到的 MPI 信息,方便调试
if(MPI_FOUND)
    message(STATUS "Successfully found MPI:")
    message(STATUS "  MPI Version: ${MPI_VERSION}")
    if(MPI_CXX_FOUND)
        message(STATUS "  C++ MPI Compiler: ${MPI_CXX_COMPILER}")
        message(STATUS "  C++ MPI Includes: ${MPI_CXX_INCLUDE_PATH}")
        message(STATUS "  C++ MPI Libraries: ${MPI_CXX_LIBRARIES}")
    else()
        message(WARNING "C++ MPI support not found, but general MPI was found. This might indicate an issue if you plan to use C++.")
    endif()
    if(MPI_EXECUTABLE)
        message(STATUS "  MPI Launcher: ${MPI_EXECUTABLE}")
    endif()
else()
    message(FATAL_ERROR "MPI support not found. Please ensure an MPI implementation is installed and configured.")
endif()

# 添加一个可执行目标,使用我们的 C++ 源文件
add_executable(helloMPI helloMPI.cpp)

# 将可执行目标链接到 MPI 库。
# MPI::MPI_CXX 是 FindMPI 模块提供的导入目标,它包含了所有必要的编译和链接信息。
target_link_libraries(helloMPI
  PUBLIC
      MPI::MPI_CXX
)

配置、构建并运行这个 CMake 项目:

(1)创建构建目录并进入,运行 CMake 进行配置:

mkdir build
cd build
cmake ..

重要提示: 尽管 FindMPI 模块通常会正确地检测到 MPI 编译器包装器,但在某些情况下,如果系统默认的 C++ 编译器不是 MPI 包装器(例如,默认是 g++ 而不是 mpicxx),需要在 CMake 配置时显式指定编译器:

cmake .. -D CMAKE_CXX_COMPILER=$(which mpicxx)
# 或者直接指定路径,例如:
# cmake .. -D CMAKE_CXX_COMPILER=/usr/bin/mpicxx

这确保 CMake 使用 MPI 提供的包装器进行编译,从而正确地处理 MPI 特定的头文件和库。

如果 CMake 成功找到 MPI 支持,看到类似以下输出:

-- The CXX compiler identification is GNU 11.3.0 
-- Check for working CXX compiler: /usr/bin/c++ 
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Successfully found MPI:
--   MPI Version: 3.1
--   C++ MPI Compiler: /usr/bin/mpicxx 
--   C++ MPI Includes: /usr/lib/x86_64-linux-gnu/openmpi/include 
--   C++ MPI Libraries: /usr/lib/x86_64-linux-gnu/openmpi/lib/libmpi_cxx.so;/usr/lib/x86_64-linux-gnu/openmpi/lib/libmpi.so 
--   MPI Launcher: /usr/bin/mpirun 
-- Configuring done
-- Generating done

执行构建:

cmake --build .

为了并行执行这个程序,使用 MPI 启动器(例如 mpirunmpiexec)。 -np 参数指定要启动的进程数量。

mpirun -np 2 ./helloMPI

看到类似以下输出(进程的打印顺序可能不同):

Hello world from processor larry, rank 1 out of 2 processors
Hello world from processor larry, rank 0 out of 2 processors

这表明两个独立的 MPI 进程已成功启动并在不同的排名上执行了程序。

3.3、工作原理深度解析

(1)MPI 编译器包装器 (mpicxx, mpicc, mpifort): MPI 编译器包装器是 MPI 环境的一个核心抽象。它们不是真正的编译器,而是对底层系统编译器(如 GCC、Clang、Intel C++ Compiler)的封装。当使用 mpicxx 来编译 MPI C++ 程序时,它实际上会调用底层的 C++ 编译器,并自动添加所有必需的编译标志(例如,用于查找 MPI 头文件的 -I 路径)和链接标志(例如,用于链接 MPI 库的 -L 路径和 -l 选项)。

这种机制极大地简化了 MPI 程序的编译过程,因为无需手动管理复杂的编译器和链接器选项。可以通过 mpicxx --showme:compilempicxx --showme:link 命令来查看它们在幕后添加的编译和链接标志。

  • mpicxx --showme:compile 会显示编译阶段需要添加的标志,例如线程支持和 MPI 头文件的包含路径。
  • mpicxx --showme:link 会显示链接阶段需要添加的标志,例如线程支持、运行时库搜索路径 (-rpath)、库搜索路径 (-L) 和要链接的 MPI 库 (-lmpi_cxx, -lmpi)。

(2)find_package(MPI REQUIRED) 这个命令调用 CMake 内置的 FindMPI.cmake 模块。该模块会尝试在系统上定位 MPI 的安装路径,并识别其组件(编译器包装器、库、头文件)。它会执行一系列测试(例如,尝试编译一个简单的 MPI 程序)来验证 MPI 环境是否可用。 如果成功找到,它会设置 MPI_FOUND 为真,并填充一系列与 MPI 相关的变量,如前所述的 MPI_VERSION, MPI_CXX_COMPILER, MPI_CXX_INCLUDE_PATH, MPI_CXX_LIBRARIES 等。

(3)导入目标 (MPI::MPI_CXX): 这是 CMake 3.9 及更高版本中处理外部库依赖的现代化和推荐方式。当 find_package(MPI) 成功时,它会创建一个或多个导入目标。对于 C++,这个导入目标通常是 MPI::MPI_CXX

导入目标是一个“伪目标”,它并不代表项目中的源代码,而是代表一个外部已经存在或将在构建时可用的库。这个导入目标封装了所有与 MPI 相关的接口属性,例如:

  • INTERFACE_COMPILE_OPTIONS:包含启用 MPI 所需的编译器标志。
  • INTERFACE_INCLUDE_DIRECTORIES:包含 MPI 头文件路径。
  • INTERFACE_LINK_LIBRARIES:包含 MPI 运行时库的链接信息。

通过使用 target_link_libraries(helloMPI PUBLIC MPI::MPI_CXX),告诉 CMake:

  • helloMPI 目标需要链接 MPI::MPI_CXX 所代表的 MPI 库。
  • PUBLIC 关键字表示 MPI::MPI_CXX 的所有接口属性(编译选项、包含目录、链接库)不仅会应用于 helloMPI 目标本身的编译和链接,也会传递给任何直接或间接依赖 helloMPI 目标的其他目标。这使得依赖管理更加简洁和自动化。

(4)这种方法极大地简化了 CMakeLists.txt。不再需要手动获取 MPI_CXX_COMPILE_FLAGSMPI_CXX_INCLUDE_PATHMPI_CXX_LIBRARIES 等变量,并将其分别传递给 target_compile_optionstarget_include_directoriestarget_link_libraries。所有的复杂性都被封装在 MPI::MPI_CXX 导入目标中,CMake 会自动处理正确的编译标志、包含目录和链接库。这使得构建脚本更简洁、更健壮,并减少了因手动管理编译器特定标志而导致的错误。

3.4、兼容旧版

对于低于 CMake 3.9 的版本,FindMPI 模块不会提供导入目标。在这种情况下,要手动将 FindMPI 模块设置的变量应用到目标上。这涉及使用 MPI_CXX_COMPILE_FLAGSMPI_CXX_INCLUDE_PATHMPI_CXX_LIBRARIES 变量:

# 仅适用于 CMake < 3.9 的旧方法
# 查找 MPI
find_package(MPI REQUIRED)

add_executable(helloMPI helloMPI.cpp)

# 手动添加编译器选项以启用 MPI
target_compile_options(helloMPI
  PUBLIC
      ${MPI_CXX_COMPILE_FLAGS}
  )

# 手动添加 MPI 头文件目录
target_include_directories(helloMPI
  PUBLIC
      ${MPI_CXX_INCLUDE_PATH}
  )

# 手动链接 MPI 库
target_link_libraries(helloMPI
  PUBLIC
      ${MPI_CXX_LIBRARIES}
  )

强烈建议升级到 CMake 3.9 或更高版本,并采用现代化导入目标的方式,因为它更加简洁、健壮,并且更好地处理了跨编译器和平台差异。

通过正确配置 CMake 来检测和链接 MPI,可以轻松地在 C/C++ 项目中构建分布式并行应用程序,从而充分利用集群和多节点系统的强大计算能力。

四、总结

详细阐述了如何在 CMake 构建系统中有效地检测、配置和使用 OpenMP 和 MPI 这两种关键的并行编程模型。通过使用 CMake 的现代特性,特别是导入目标 (OpenMP::OpenMP_CXXMPI::MPI_CXX),可以极大地简化 OpenMP 和 MPI 程序的编译和链接过程,并提高构建脚本的可维护性和可移植性。

深入探讨了 MPI 编译器包装器的工作原理,以及 FindMPI 模块如何检测 MPI 环境并设置相关的 CMake 变量。同时,为了兼容旧版 CMake,也提供了手动配置 OpenMP 和 MPI 的方法,尽管强烈建议使用现代的导入目标方式。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Lion 莱恩呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值