还在为并行编程头疼?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 支持的步骤:
-
首先指定项目所需的最低 CMake 版本。对于 OpenMP 的现代支持(通过导入目标),建议使用 CMake 3.9 或更高版本。然后声明项目名称和使用的编程语言(C++)。
# 设置项目所需的最低 CMake 版本。建议 3.9 或更高以获得 OpenMP 的现代支持。 cmake_minimum_required(VERSION 3.10 FATAL_ERROR) # 定义项目名称,指定使用的语言为 C++ project(OpenMP_Example LANGUAGES CXX) -
为了确保代码的可移植性和使用现代 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) -
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() -
定义可执行目标,并将源代码文件 (
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、工作原理深度解析
-
find_package(OpenMP REQUIRED): 这个命令调用 CMake 内置的FindOpenMP.cmake模块。该模块负责检测系统上 C、C++ 编译器对 OpenMP 的支持。它会检查编译器是否支持 OpenMP 标志(例如 GCC/Clang 的-fopenmp,MSVC 的/openmp),并尝试编译和链接一个小的测试程序来验证 OpenMP 功能是否可用。如果成功找到,它会设置OpenMP_FOUND为真,并设置一些变量,例如OpenMP_CXX_FLAGS包含 C++ 编译器启用 OpenMP 所需的编译标志。 -
导入目标 (
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 的编译和链接要求。这大大简化了大型项目的依赖管理。
-
这种方法极大地简化了
CMakeLists.txt。不再需要手动获取OpenMP_CXX_FLAGS并将其传递给target_compile_options和target_link_libraries。所有的复杂性都被封装在OpenMP::OpenMP_CXX导入目标中,CMake 会自动处理正确的编译标志和链接库。这使得构建脚本更简洁、更健壮,并减少了因手动管理编译器特定标志而导致的错误。 -
如果想查看导入目标
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 实现通常包括以下几个核心组件:
- 运行时库: 提供 MPI 函数的实际实现,供应用程序调用。
- 头文件: 包含 MPI 函数的声明和常量的定义,供 C/C++ 和 Fortran 编译器使用。
- 编译器包装器: 这是 MPI 环境的一个重要特征。它们是特殊的脚本或程序,用于封装底层的 C/C++ 或 Fortran 编译器。例如,
mpicxx(或mpiCC/mpic++) 用于 C++,mpicc用于 C,mpifort用于 Fortran。这些包装器会自动添加编译和链接 MPI 程序所需的额外编译器标志、头文件路径和库路径,极大地简化了 MPI 程序的编译过程。 - 启动器: 用于启动 MPI 程序的并行执行。这些工具(如
mpirun、mpiexec或orterun)负责在指定的进程数量和节点上启动程序的多个副本,并建立它们之间的通信基础设施。
本节将详细展示如何在 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。
-
首先,定义项目所需的最低 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) -
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 库路径)。 -
定义可执行目标,并将源代码文件 (
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 启动器(例如 mpirun 或 mpiexec)。 -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:compile 和 mpicxx --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_FLAGS、MPI_CXX_INCLUDE_PATH 和 MPI_CXX_LIBRARIES 等变量,并将其分别传递给 target_compile_options、target_include_directories 和 target_link_libraries。所有的复杂性都被封装在 MPI::MPI_CXX 导入目标中,CMake 会自动处理正确的编译标志、包含目录和链接库。这使得构建脚本更简洁、更健壮,并减少了因手动管理编译器特定标志而导致的错误。
3.4、兼容旧版
对于低于 CMake 3.9 的版本,FindMPI 模块不会提供导入目标。在这种情况下,要手动将 FindMPI 模块设置的变量应用到目标上。这涉及使用 MPI_CXX_COMPILE_FLAGS、MPI_CXX_INCLUDE_PATH 和 MPI_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_CXX 和 MPI::MPI_CXX),可以极大地简化 OpenMP 和 MPI 程序的编译和链接过程,并提高构建脚本的可维护性和可移植性。
深入探讨了 MPI 编译器包装器的工作原理,以及 FindMPI 模块如何检测 MPI 环境并设置相关的 CMake 变量。同时,为了兼容旧版 CMake,也提供了手动配置 OpenMP 和 MPI 的方法,尽管强烈建议使用现代的导入目标方式。

3531

被折叠的 条评论
为什么被折叠?



