CMake 秘籍(二)(3)https://developer.aliyun.com/article/1525090
它是如何工作的
我们的简单示例似乎有效:代码已编译并链接,并且在运行于多个核心时我们观察到了加速。加速不是OMP_NUM_THREADS
的完美倍数并不是本教程的关注点,因为我们专注于需要 OpenMP 的项目中的 CMake 方面。我们发现由于FindOpenMP
模块提供的导入目标,链接 OpenMP 极其简洁:
target_link_libraries(example PUBLIC OpenMP::OpenMP_CXX )
我们不必担心编译标志或包含目录——这些设置和依赖关系都编码在库OpenMP::OpenMP_CXX
的定义中,该库属于IMPORTED
类型。正如我们在第 3 个配方中提到的,构建和链接静态和共享库,在第一章,从简单的可执行文件到库中,IMPORTED
库是伪目标,它们完全编码了外部依赖的使用要求。要使用 OpenMP,需要设置编译器标志、包含目录和链接库。所有这些都作为属性设置在OpenMP::OpenMP_CXX
目标上,并通过使用target_link_libraries
命令间接应用于我们的example
目标。这使得在我们的 CMake 脚本中使用库变得非常容易。我们可以使用cmake_print_properties
命令打印接口的属性,该命令由CMakePrintHelpers.cmake
标准模块提供:
include(CMakePrintHelpers) cmake_print_properties( TARGETS OpenMP::OpenMP_CXX PROPERTIES INTERFACE_COMPILE_OPTIONS INTERFACE_INCLUDE_DIRECTORIES INTERFACE_LINK_LIBRARIES )
请注意,所有感兴趣的属性都带有前缀INTERFACE_
,因为这些属性的使用要求适用于任何希望接口并使用 OpenMP 目标的目标。
对于 CMake 版本低于 3.9 的情况,我们需要做更多的工作:
add_executable(example example.cpp) target_compile_options(example PUBLIC ${OpenMP_CXX_FLAGS} ) set_target_properties(example PROPERTIES LINK_FLAGS ${OpenMP_CXX_FLAGS} )
对于 CMake 版本低于 3.5 的情况,我们可能需要为 Fortran 项目明确定义编译标志。
在本配方中,我们讨论了 C++和 Fortran,但论点和方法同样适用于 C 项目。
检测 MPI 并行环境
本配方的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-06
找到,并包含 C++和 C 的示例。该配方适用于 CMake 版本 3.9(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-06
,我们还提供了一个与 CMake 3.5 兼容的 C 示例。
与 OpenMP 共享内存并行性的一种替代且通常互补的方法是消息传递接口(MPI),它已成为在分布式内存系统上并行执行程序的事实标准。尽管现代 MPI 实现也允许共享内存并行性,但在高性能计算中,典型的方法是使用 OpenMP 在计算节点内结合 MPI 跨计算节点。MPI 标准的实现包括以下内容:
- 运行时库。
- 头文件和 Fortran 90 模块。
- 编译器包装器,它调用用于构建 MPI 库的编译器,并带有额外的命令行参数来处理包含目录和库。通常,可用的编译器包装器包括
mpic++
/mpiCC
/mpicxx
用于 C++,mpicc
用于 C,以及mpifort
用于 Fortran。 - MPI 启动器:这是您应该调用的程序,用于启动编译代码的并行执行。其名称取决于实现,通常是以下之一:
mpirun
、mpiexec
或orterun
。
本示例将展示如何在系统上找到合适的 MPI 实现,以便编译简单的 MPI“Hello, World”程序。
准备工作
本示例代码(hello-mpi.cpp
,从www.mpitutorial.com
下载),我们将在本示例中编译,将初始化 MPI 库,让每个进程打印其名称,并最终关闭库:
#include <iostream> #include <mpi.h> int main(int argc, char **argv) { // Initialize the MPI environment. The two arguments to MPI Init are not // currently used by MPI implementations, but are there in case future // implementations might need the arguments. MPI_Init(NULL, NULL); // Get the number of processes int world_size; MPI_Comm_size(MPI_COMM_WORLD, &world_size); // Get the rank of the process int world_rank; MPI_Comm_rank(MPI_COMM_WORLD, &world_rank); // Get the name of the processor char processor_name[MPI_MAX_PROCESSOR_NAME]; int name_len; MPI_Get_processor_name(processor_name, &name_len); // Print off a hello world message std::cout << "Hello world from processor " << processor_name << ", rank " << world_rank << " out of " << world_size << " processors" << std::endl; // Finalize the MPI environment. No more MPI calls can be made after this MPI_Finalize(); }
如何操作
在本示例中,我们旨在找到 MPI 实现:库、头文件、编译器包装器和启动器。为此,我们将利用FindMPI.cmake
标准 CMake 模块:
- 首先,我们定义最小 CMake 版本、项目名称、支持的语言和语言标准:
cmake_minimum_required(VERSION 3.9 FATAL_ERROR) project(recipe-06 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 然后我们调用
find_package
来定位 MPI 实现:
find_package(MPI REQUIRED)
- 我们定义可执行文件的名称和源代码,并且与前面的示例类似,链接到导入的目标:
add_executable(hello-mpi hello-mpi.cpp) target_link_libraries(hello-mpi PUBLIC MPI::MPI_CXX )
- 让我们配置并构建可执行文件:
$ mkdir -p build $ cd build $ cmake -D CMAKE_CXX_COMPILER=mpicxx .. -- ... -- Found MPI_CXX: /usr/lib/openmpi/libmpi_cxx.so (found version "3.1")
-- Found MPI: TRUE (found version "3.1") -- ... $ cmake --build .
- 为了并行执行此程序,我们使用
mpirun
启动器(在这种情况下,使用两个任务):
$ mpirun -np 2 ./hello-mpi Hello world from processor larry, rank 1 out of 2 processors Hello world from processor larry, rank 0 out of 2 processors
工作原理
请记住,编译器包装器是围绕编译器的一层薄层,用于构建 MPI 库。在底层,它将调用相同的编译器,并为其添加额外的参数,如包含路径和库,以成功构建并行程序。
包装器在编译和链接源文件时实际应用哪些标志?我们可以使用编译器包装器的--showme
选项来探测这一点。要找出编译器标志,我们可以使用:
$ mpicxx --showme:compile -pthread
要找出链接器标志,我们使用以下方法:
$ mpicxx --showme:link -pthread -Wl,-rpath -Wl,/usr/lib/openmpi -Wl,--enable-new-dtags -L/usr/lib/openmpi -lmpi_cxx -lmpi
与前一个 OpenMP 示例类似,我们发现链接到 MPI 非常简洁,这得益于相对现代的FindMPI
模块提供的导入目标:
target_link_libraries(hello-mpi PUBLIC MPI::MPI_CXX )
我们不必担心编译标志或包含目录 - 这些设置和依赖关系已经作为INTERFACE
类型属性编码在 CMake 提供的IMPORTED
目标中。
正如在前一个示例中讨论的,对于 CMake 版本低于 3.9 的情况,我们需要做更多的工作:
add_executable(hello-mpi hello-mpi.c) target_compile_options(hello-mpi PUBLIC ${MPI_CXX_COMPILE_FLAGS} ) target_include_directories(hello-mpi PUBLIC ${MPI_CXX_INCLUDE_PATH} ) target_link_libraries(hello-mpi PUBLIC ${MPI_CXX_LIBRARIES} )
在本示例中,我们讨论了 C++,但参数和方法同样适用于 C 或 Fortran 项目。
检测 Eigen 库
本示例的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-07
找到,包含一个 C++示例。本示例适用于 CMake 版本 3.9(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-07
,我们还提供了一个与 CMake 3.5 兼容的 C++示例。
BLAS 库为涉及矩阵和向量的常见操作提供了一个标准化的接口。然而,这个接口是针对 Fortran 语言标准化的。虽然我们已经展示了如何从 C++中或多或少直接使用这些库,但在现代 C++程序中可能希望有一个更高层次的接口。
Eigen 库作为头文件使用模板编程来提供这样的接口。其矩阵和向量类型易于使用,甚至在编译时提供类型检查,以确保不混合不兼容的矩阵维度。密集和稀疏矩阵操作,如矩阵-矩阵乘积、线性系统求解器和特征值问题,也使用表达式模板实现效率。从版本 3.3 开始,Eigen 可以链接到 BLAS 和 LAPACK 库,这提供了灵活性,可以将某些操作卸载到这些库中提供的实现以获得额外的性能。
本配方将展示如何找到 Eigen 库,并指示它使用 OpenMP 并行化并将部分工作卸载到 BLAS 库。
准备就绪
在本例中,我们将编译一个程序,该程序分配一个随机方阵和从命令行传递的维度的向量。然后,我们将使用 LU 分解求解线性系统Ax=b。我们将使用以下源代码(linear-algebra.cpp
):
#include <chrono> #include <cmath> #include <cstdlib> #include <iomanip> #include <iostream> #include <vector> #include <Eigen/Dense> int main(int argc, char **argv) { if (argc != 2) { std::cout << "Usage: ./linear-algebra dim" << std::endl; return EXIT_FAILURE; } std::chrono::time_point<std::chrono::system_clock> start, end; std::chrono::duration<double> elapsed_seconds; std::time_t end_time; std::cout << "Number of threads used by Eigen: " << Eigen::nbThreads() << std::endl; // Allocate matrices and right-hand side vector start = std::chrono::system_clock::now(); int dim = std::atoi(argv[1]); Eigen::MatrixXd A = Eigen::MatrixXd::Random(dim, dim); Eigen::VectorXd b = Eigen::VectorXd::Random(dim); end = std::chrono::system_clock::now(); // Report times elapsed_seconds = end - start; end_time = std::chrono::system_clock::to_time_t(end); std::cout << "matrices allocated and initialized " << std::put_time(std::localtime(&end_time), "%a %b %d %Y %r\n") << "elapsed time: " << elapsed_seconds.count() << "s\n"; start = std::chrono::system_clock::now(); // Save matrix and RHS Eigen::MatrixXd A1 = A; Eigen::VectorXd b1 = b; end = std::chrono::system_clock::now(); end_time = std::chrono::system_clock::to_time_t(end); std::cout << "Scaling done, A and b saved " << std::put_time(std::localtime(&end_time), "%a %b %d %Y %r\n") << "elapsed time: " << elapsed_seconds.count() << "s\n"; start = std::chrono::system_clock::now(); Eigen::VectorXd x = A.lu().solve(b); end = std::chrono::system_clock::now(); // Report times elapsed_seconds = end - start; end_time = std::chrono::system_clock::to_time_t(end); double relative_error = (A * x - b).norm() / b.norm(); std::cout << "Linear system solver done " << std::put_time(std::localtime(&end_time), "%a %b %d %Y %r\n") << "elapsed time: " << elapsed_seconds.count() << "s\n"; std::cout << "relative error is " << relative_error << std::endl; return 0; }
矩阵-向量乘法和 LU 分解在 Eigen 中实现,但可以选择卸载到 BLAS 和 LAPACK 库。在本配方中,我们只考虑卸载到 BLAS 库。
如何做到这一点
在本项目中,我们将找到 Eigen 和 BLAS 库,以及 OpenMP,并指示 Eigen 使用 OpenMP 并行化,并将部分线性代数工作卸载到 BLAS 库:
- 我们首先声明 CMake 的最低版本、项目名称以及使用 C++11 语言:
cmake_minimum_required(VERSION 3.9 FATAL_ERROR) project(recipe-07 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 我们还请求 OpenMP,因为 Eigen 可以利用共享内存并行性进行密集操作:
find_package(OpenMP REQUIRED)
- 我们通过调用
find_package
在CONFIG
模式下搜索 Eigen(我们将在下一节讨论这一点):
find_package(Eigen3 3.3 REQUIRED CONFIG)
- 如果找到 Eigen,我们会打印出有帮助的状态消息。请注意,我们正在使用
Eigen3::Eigen
目标。正如我们在前两个配方中学到的,这是一个IMPORTED
目标,由 Eigen 分发的原生 CMake 脚本提供:
if(TARGET Eigen3::Eigen) message(STATUS "Eigen3 v${EIGEN3_VERSION_STRING} found in ${EIGEN3_INCLUDE_DIR}") endif()
- 接下来,我们为我们的源文件声明一个可执行目标:
add_executable(linear-algebra linear-algebra.cpp)
- 然后我们找到 BLAS。请注意,依赖项现在不是必需的:
find_package(BLAS)
- 如果找到 BLAS,我们为可执行目标设置相应的编译定义和链接库:
if(BLAS_FOUND) message(STATUS "Eigen will use some subroutines from BLAS.") message(STATUS "See: http://eigen.tuxfamily.org/dox-devel/TopicUsingBlasLapack.html") target_compile_definitions(linear-algebra PRIVATE EIGEN_USE_BLAS ) target_link_libraries(linear-algebra PUBLIC ${BLAS_LIBRARIES} ) else() message(STATUS "BLAS not found. Using Eigen own functions") endif()
- 最后,我们链接到导入的
Eigen3::Eigen
和OpenMP::OpenMP_CXX
目标。这足以设置所有必要的编译和链接标志:
target_link_libraries(linear-algebra PUBLIC Eigen3::Eigen OpenMP::OpenMP_CXX )
- 我们现在已经准备好配置项目:
$ mkdir -p build $ cd build $ cmake .. -- ... -- Found OpenMP_CXX: -fopenmp (found version "4.5") -- Found OpenMP: TRUE (found version "4.5") -- Eigen3 v3.3.4 found in /usr/include/eigen3 -- ... -- Found BLAS: /usr/lib/libblas.so -- Eigen will use some subroutines from BLAS. -- See: http://eigen.tuxfamily.org/dox-devel/TopicUsingBlasLapack.html
- 最后,我们编译并测试代码。请注意,在这种情况下,二进制文件使用了四个可用线程:
$ cmake --build . $ ./linear-algebra 1000 Number of threads used by Eigen: 4 matrices allocated and initialized Sun Jun 17 2018 11:04:20 AM elapsed time: 0.0492328s Scaling done, A and b saved Sun Jun 17 2018 11:04:20 AM elapsed time: 0.0492328s Linear system solver done Sun Jun 17 2018 11:04:20 AM elapsed time: 0.483142s relative error is 4.21946e-13
它是如何工作的
Eigen 提供了原生的 CMake 支持,这使得使用它来设置 C++ 项目变得简单。从版本 3.3 开始,Eigen 提供了 CMake 模块,导出适当的 target,即 Eigen3::Eigen
,我们在这里使用了它。
您可能已经注意到 find_package
命令的 CONFIG
选项。这向 CMake 发出信号,表明包搜索不会通过 FindEigen3.cmake
模块进行,而是通过 Eigen3 包在标准位置提供的 Eigen3Config.cmake
、Eigen3ConfigVersion.cmake
和 Eigen3Targets.cmake
文件进行,即 /share/eigen3/cmake
。这种包位置模式称为“Config”模式,比我们迄今为止使用的 Find.cmake
方法更灵活。有关“Module”模式与“Config”模式的更多信息,请查阅官方文档:cmake.org/cmake/help/v3.5/command/find_package.html
。
还要注意,尽管 Eigen3、BLAS 和 OpenMP 依赖项被声明为 PUBLIC
依赖项,但 EIGEN_USE_BLAS
编译定义被声明为 PRIVATE
。我们不是直接链接可执行文件,而是可以将库依赖项收集到一个单独的库目标中。使用 PUBLIC
/PRIVATE
关键字,我们可以调整相应标志和定义对库目标依赖项的可见性。
还有更多
CMake 会在预定义的位置层次结构中查找配置模块。首先是 CMAKE_PREFIX_PATH
,而 _DIR
是下一个搜索路径。因此,如果 Eigen3 安装在非标准位置,我们可以使用两种替代方法来告诉 CMake 在哪里查找它:
- 通过传递 Eigen3 的安装前缀作为
CMAKE_PREFIX_PATH
:
$ cmake -D CMAKE_PREFIX_PATH=<installation-prefix> ..
- 通过传递配置文件的位置作为
Eigen3_DIR
:
$ cmake -D Eigen3_DIR=<installation-prefix>/share/eigen3/cmake/
检测 Boost 库
本食谱的代码可在 github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-08
获取,并包含一个 C++ 示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
Boost 库是一系列通用目的的 C++ 库。这些库提供了许多现代 C++ 项目中可能必不可少的特性,而这些特性在 C++ 标准中尚未提供。例如,Boost 提供了元编程、处理可选参数和文件系统操作等组件。许多这些库后来被 C++11、C++14 和 C++17 标准采纳,但对于需要保持与旧编译器兼容性的代码库,许多 Boost 组件仍然是首选库。
本食谱将向您展示如何检测并链接 Boost 库的某些组件。
准备就绪
我们将编译的源代码是 Boost 提供的文件系统库的示例之一,用于与文件系统交互。该库方便地跨平台,并将操作系统与文件系统的差异抽象成一个连贯的高级 API。以下示例代码(path-info.cpp
)将接受一个路径作为参数,并将其组件的报告打印到屏幕上:
#include <iostream> #include <boost/filesystem.hpp> using namespace std; using namespace boost::filesystem; const char *say_what(bool b) { return b ? "true" : "false"; } int main(int argc, char *argv[]) { if (argc < 2) { cout << "Usage: path_info path-element [path-element...]\n" "Composes a path via operator/= from one or more path-element arguments\n" "Example: path_info foo/bar baz\n" #ifdef BOOST_POSIX_API " would report info about the composed path foo/bar/baz\n"; #else // BOOST_WINDOWS_API " would report info about the composed path foo/bar\\baz\n"; #endif return 1; } path p; for (; argc > 1; --argc, ++argv) p /= argv[1]; // compose path p from the command line arguments cout << "\ncomposed path:\n"; cout << " operator<<()---------: " << p << "\n"; cout << " make_preferred()-----: " << p.make_preferred() << "\n"; cout << "\nelements:\n"; for (auto element : p) cout << " " << element << '\n'; cout << "\nobservers, native format:" << endl; #ifdef BOOST_POSIX_API cout << " native()-------------: " << p.native() << endl; cout << " c_str()--------------: " << p.c_str() << endl; #else // BOOST_WINDOWS_API wcout << L" native()-------------: " << p.native() << endl; wcout << L" c_str()--------------: " << p.c_str() << endl; #endif cout << " string()-------------: " << p.string() << endl; wcout << L" wstring()------------: " << p.wstring() << endl; cout << "\nobservers, generic format:\n"; cout << " generic_string()-----: " << p.generic_string() << endl; wcout << L" generic_wstring()----: " << p.generic_wstring() << endl; cout << "\ndecomposition:\n"; cout << " root_name()----------: " << p.root_name() << '\n'; cout << " root_directory()-----: " << p.root_directory() << '\n'; cout << " root_path()----------: " << p.root_path() << '\n'; cout << " relative_path()------: " << p.relative_path() << '\n'; cout << " parent_path()--------: " << p.parent_path() << '\n'; cout << " filename()-----------: " << p.filename() << '\n'; cout << " stem()---------------: " << p.stem() << '\n'; cout << " extension()----------: " << p.extension() << '\n'; cout << "\nquery:\n"; cout << " empty()--------------: " << say_what(p.empty()) << '\n'; cout << " is_absolute()--------: " << say_what(p.is_absolute()) << '\n'; cout << " has_root_name()------: " << say_what(p.has_root_name()) << '\n'; cout << " has_root_directory()-: " << say_what(p.has_root_directory()) << '\n'; cout << " has_root_path()------: " << say_what(p.has_root_path()) << '\n'; cout << " has_relative_path()--: " << say_what(p.has_relative_path()) << '\n'; cout << " has_parent_path()----: " << say_what(p.has_parent_path()) << '\n'; cout << " has_filename()-------: " << say_what(p.has_filename()) << '\n'; cout << " has_stem()-----------: " << say_what(p.has_stem()) << '\n'; cout << " has_extension()------: " << say_what(p.has_extension()) << '\n'; return 0; }
CMake 秘籍(二)(5)https://developer.aliyun.com/article/1525092