CMake 秘籍(二)(2)https://developer.aliyun.com/article/1525089
它是如何工作的
在这个 CMake 脚本中,有三个新的 CMake 命令:execute_process
和add_custom_command
,它们总是可用的,以及find_package_handle_standard_args
,它需要include(FindPackageHandleStandardArgs)
。
execute_process
命令将执行一个或多个作为当前发出的 CMake 命令的子进程的命令。最后一个子进程的返回值将被保存到作为参数传递给RESULT_VARIABLE
的变量中,而标准输出和标准错误管道的内容将被保存到作为参数传递给OUTPUT_VARIABLE
和ERROR_VARIABLE
的变量中。execute_process
允许我们执行任意命令,并使用它们的结果来推断我们系统的配置。在我们的例子中,我们首先使用它来确保 NumPy 可用,然后获取模块的版本。
find_package_handle_standard_args
命令提供了处理与在给定系统上找到的程序和库相关的常见操作的标准工具。版本相关的选项,REQUIRED
和EXACT
,在引用此命令时都得到了正确处理,无需进一步的 CMake 代码。额外的选项QUIET
和COMPONENTS
,我们很快就会遇到,也由这个 CMake 命令在幕后处理。在这个脚本中,我们使用了以下内容:
include(FindPackageHandleStandardArgs) find_package_handle_standard_args(NumPy FOUND_VAR NumPy_FOUND REQUIRED_VARS NumPy VERSION_VAR _numpy_version )
当所有必需的变量都被设置为有效的文件路径(NumPy
)时,该命令将设置变量以发出模块已被找到的信号(NumPy_FOUND
)。它还将设置版本到传递的版本变量(_numpy_version
),并为用户打印出状态消息:
-- Found NumPy: /usr/lib/python3.6/site-packages/numpy (found version "1.14.3")
在本食谱中,我们没有进一步使用这些变量。我们可以做的是,如果NumPy_FOUND
被返回为FALSE
,则停止配置。
最后,我们应该对将use_numpy.py
复制到构建目录的代码段进行评论:
add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py ) target_sources(pure-embedding PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py )
我们本可以使用file(COPY ...)
命令来实现复制。在这里,我们选择使用add_custom_command
以确保每次文件更改时都会复制文件,而不仅仅是在我们首次运行配置时。我们将在第五章*, 配置时间和构建时间操作*中更详细地回顾add_custom_command
。还请注意target_sources
命令,它将依赖项添加到${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
;这样做是为了确保构建pure-embedding
目标会触发前面的自定义命令。
检测 BLAS 和 LAPACK 数学库
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-04
找到,并包含一个 C++示例。本食谱适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
许多数值代码严重依赖于矩阵和向量运算。例如,考虑矩阵-向量和矩阵-矩阵乘积、线性方程组的解、特征值和特征向量的计算或奇异值分解。这些操作可能在代码库中无处不在,或者必须在大数据集上运行,以至于高效的实现变得绝对必要。幸运的是,有专门为此目的的库:基本线性代数子程序(BLAS)和线性代数包(LAPACK)提供了标准API,用于涉及线性代数操作的许多任务。不同的供应商提供不同的实现,但它们都共享相同的 API。尽管数学库底层实现所用的实际编程语言随时间而变化(Fortran、C、汇编),但留下的历史痕迹是 Fortran 调用约定。在本食谱中,我们的任务将是链接到这些库,并展示如何无缝地使用用不同语言编写的库,考虑到上述调用约定。
准备工作
为了演示数学库的检测和链接,我们希望编译一个 C++程序,该程序接受矩阵维数作为命令行输入,生成一个随机方阵A,一个随机向量b,并解决随之而来的线性方程组:Ax = b。此外,我们将用一个随机因子缩放随机向量b。我们需要使用的子程序是来自 BLAS 的DSCAL
,用于执行缩放,以及来自 LAPACK 的DGESV
,用于找到线性方程组的解。示例 C++代码的列表包含在(linear-algebra.cpp
)中:
#include "CxxBLAS.hpp" #include "CxxLAPACK.hpp" #include <iostream> #include <random> #include <vector> int main(int argc, char **argv) { if (argc != 2) { std::cout << "Usage: ./linear-algebra dim" << std::endl; return EXIT_FAILURE; } // Generate a uniform distribution of real number between -1.0 and 1.0 std::random_device rd; std::mt19937 mt(rd()); std::uniform_real_distribution<double> dist(-1.0, 1.0); // Allocate matrices and right-hand side vector int dim = std::atoi(argv[1]); std::vector<double> A(dim * dim); std::vector<double> b(dim); std::vector<int> ipiv(dim); // Fill matrix and RHS with random numbers between -1.0 and 1.0 for (int r = 0; r < dim; r++) { for (int c = 0; c < dim; c++) { A[r + c * dim] = dist(mt); } b[r] = dist(mt); } // Scale RHS vector by a random number between -1.0 and 1.0 C_DSCAL(dim, dist(mt), b.data(), 1); std::cout << "C_DSCAL done" << std::endl; // Save matrix and RHS std::vector<double> A1(A); std::vector<double> b1(b); int info; info = C_DGESV(dim, 1, A.data(), dim, ipiv.data(), b.data(), dim); std::cout << "C_DGESV done" << std::endl; std::cout << "info is " << info << std::endl; double eps = 0.0; for (int i = 0; i < dim; ++i) { double sum = 0.0; for (int j = 0; j < dim; ++j) sum += A1[i + j * dim] * b[j]; eps += std::abs(b1[i] - sum); } std::cout << "check is " << eps << std::endl; return 0; }
我们使用 C++11 中引入的随机库来生成-1.0 到 1.0 之间的随机分布。C_DSCAL
和C_DGESV
是 BLAS 和 LAPACK 库的接口,分别负责名称修饰,以便从不同的编程语言调用这些函数。这是在以下接口文件中与我们将进一步讨论的 CMake 模块结合完成的。
文件CxxBLAS.hpp
使用extern "C"
链接包装 BLAS 例程:
#pragma once #include "fc_mangle.h" #include <cstddef> #ifdef __cplusplus extern "C" { #endif extern void DSCAL(int *n, double *alpha, double *vec, int *inc); #ifdef __cplusplus } #endif void C_DSCAL(size_t length, double alpha, double *vec, int inc);
相应的实现文件CxxBLAS.cpp
包含:
#include "CxxBLAS.hpp" #include <climits> // see http://www.netlib.no/netlib/blas/dscal.f void C_DSCAL(size_t length, double alpha, double *vec, int inc) { int big_blocks = (int)(length / INT_MAX); int small_size = (int)(length % INT_MAX); for (int block = 0; block <= big_blocks; block++) { double *vec_s = &vec[block * inc * (size_t)INT_MAX]; signed int length_s = (block == big_blocks) ? small_size : INT_MAX; ::DSCAL(&length_s, &alpha, vec_s, &inc); } }
文件CxxLAPACK.hpp
和CxxLAPACK.cpp
为 LAPACK 调用执行相应的翻译。
如何做到这一点
相应的CMakeLists.txt
包含以下构建块:
- 我们定义了最小 CMake 版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-04 LANGUAGES CXX C Fortran)
- 我们要求使用 C++11 标准:
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 此外,我们验证 Fortran 和 C/C++编译器是否能协同工作,并生成处理名称修饰的头部文件。这两项功能均由
FortranCInterface
模块提供:
include(FortranCInterface) FortranCInterface_VERIFY(CXX) FortranCInterface_HEADER( fc_mangle.h MACRO_NAMESPACE "FC_" SYMBOLS DSCAL DGESV )
- 然后,我们要求 CMake 查找 BLAS 和 LAPACK。这些是必需的依赖项:
find_package(BLAS REQUIRED) find_package(LAPACK REQUIRED)
- 接下来,我们添加一个包含我们源代码的库,用于 BLAS 和 LAPACK 包装器,并链接到
LAPACK_LIBRARIES
,这也引入了BLAS_LIBRARIES
:
add_library(math "") target_sources(math PRIVATE CxxBLAS.cpp CxxLAPACK.cpp ) target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) target_link_libraries(math PUBLIC ${LAPACK_LIBRARIES} )
- 注意,该目标的包含目录和链接库被声明为
PUBLIC
,因此任何依赖于数学库的额外目标也会在其包含目录中设置这些目录。 - 最后,我们添加一个可执行目标,并链接到
math
:
add_executable(linear-algebra "") target_sources(linear-algebra PRIVATE linear-algebra.cpp ) target_link_libraries(linear-algebra PRIVATE math )
- 在配置步骤中,我们可以专注于相关的输出:
$ mkdir -p build $ cd build $ cmake .. ... -- Detecting Fortran/C Interface -- Detecting Fortran/C Interface - Found GLOBAL and MODULE mangling -- Verifying Fortran/C Compiler Compatibility -- Verifying Fortran/C Compiler Compatibility - Success ... -- Found BLAS: /usr/lib/libblas.so ... -- A library with LAPACK API found. ...
- 最后,我们构建并测试可执行文件:
$ cmake --build . $ ./linear-algebra 1000 C_DSCAL done C_DGESV done info is 0 check is 1.54284e-10
它是如何工作的
FindBLAS.cmake
和FindLAPACK.cmake
将在标准位置查找提供标准 BLAS 和 LAPACK API 的库。对于前者,模块将查找 Fortran 实现的SGEMM
函数,用于单精度矩阵-矩阵乘法,适用于一般矩阵。对于后者,模块搜索 Fortran 实现的CHEEV
函数,用于计算复数、Hermitian 矩阵的特征值和特征向量。这些查找是通过内部编译一个调用这些函数的小程序并尝试链接到候选库来执行的。如果失败,则表明系统上没有符合要求的库。
每个编译器在生成机器代码时都会对符号进行名称混淆,不幸的是,这项操作的约定不是通用的,而是编译器依赖的。为了克服这个困难,我们使用了FortranCInterface
模块(cmake.org/cmake/help/v3.5/module/FortranCInterface.html
)来验证 Fortran 和 C/C++编译器是否能协同工作,并生成一个与所讨论编译器兼容的 Fortran-C 接口头文件fc_mangle.h
。生成的fc_mangle.h
然后必须包含在接口头文件CxxBLAS.hpp
和CxxLAPACK.hpp
中。为了使用FortranCInterface
,我们不得不在LANGUAGES
列表中添加 C 和 Fortran 支持。当然,我们可以定义自己的预处理器定义,但代价是有限的移植性。
我们将在第九章,混合语言项目中更详细地讨论 Fortran 和 C 的互操作性。
如今,许多 BLAS 和 LAPACK 的实现已经附带了一个围绕 Fortran 子程序的薄 C 层包装器。这些包装器多年来已经标准化,被称为 CBLAS 和 LAPACKE。
还有更多内容
许多数值代码严重依赖于矩阵代数操作,正确地链接到高性能的 BLAS 和 LAPACK API 实现非常重要。不同供应商在不同架构和并行环境下打包其库的方式存在很大差异。FindBLAS.cmake
和FindLAPACK.cmake
很可能无法在所有可能的情况下定位现有的库。如果发生这种情况,您可以通过 CLI 的-D
选项显式设置库。
检测 OpenMP 并行环境
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-05
找到,并包含 C++和 Fortran 示例。该食谱适用于 CMake 版本 3.9(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-05
,我们还提供了与 CMake 3.5 兼容的示例。
如今,市场上的基本任何计算机都是多核机器,对于专注于性能的程序,我们可能需要关注这些多核 CPU,并在我们的编程模型中使用并发。OpenMP 是多核 CPU 共享内存并行性的标准。现有的程序通常不需要进行根本性的修改或重写,以从 OpenMP 并行化中受益。一旦在代码中确定了性能关键部分,例如使用分析工具,程序员可以添加预处理器指令,这些指令将指示编译器为这些区域生成并行代码。
在本教程中,我们将展示如何编译包含 OpenMP 指令的程序,前提是我们使用的是支持 OpenMP 的编译器。许多 Fortran、C 和 C++编译器都可以利用 OpenMP 的并行性。CMake 对 C、C++或 Fortran 的相对较新版本提供了非常好的 OpenMP 支持。本教程将向您展示如何在使用 CMake 3.9 或更高版本时,为简单的 C++和 Fortran 程序检测并链接 OpenMP 使用导入的目标。
根据 Linux 发行版的不同,默认版本的 Clang 编译器可能不支持 OpenMP。本教程不适用于macOS,除非使用单独的 libomp 安装(iscinumpy.gitlab.io/post/omp-on-high-sierra/
)或非 Apple 版本的 Clang(例如,由 Conda 提供)或 GNU 编译器。
准备工作
C 和 C++程序可以通过包含omp.h
头文件并链接正确的库来访问 OpenMP 功能。编译器将根据性能关键部分之前的预处理器指令生成并行代码。在本教程中,我们将构建以下示例源代码(example.cpp
)。该代码将 1 到N的整数求和,其中N作为命令行参数给出:
#include <iostream> #include <omp.h> #include <string> int main(int argc, char *argv[]) { std::cout << "number of available processors: " << omp_get_num_procs() << std::endl; std::cout << "number of threads: " << omp_get_max_threads() << std::endl; auto n = std::stol(argv[1]); std::cout << "we will form sum of numbers from 1 to " << n << std::endl; // start timer auto t0 = omp_get_wtime(); auto s = 0LL; #pragma omp parallel for reduction(+ : s) for (auto i = 1; i <= n; i++) { s += i; } // stop timer auto t1 = omp_get_wtime(); std::cout << "sum: " << s << std::endl; std::cout << "elapsed wall clock time: " << t1 - t0 << " seconds" << std::endl; return 0; }
在 Fortran 中,需要使用omp_lib
模块并链接到正确的库。在性能关键部分之前的代码注释中再次可以使用并行指令。相应的example.F90
包含以下内容:
program example use omp_lib implicit none integer(8) :: i, n, s character(len=32) :: arg real(8) :: t0, t1 print *, "number of available processors:", omp_get_num_procs() print *, "number of threads:", omp_get_max_threads() call get_command_argument(1, arg) read(arg , *) n print *, "we will form sum of numbers from 1 to", n ! start timer t0 = omp_get_wtime() s = 0 !$omp parallel do reduction(+:s) do i = 1, n s = s + i end do ! stop timer t1 = omp_get_wtime() print *, "sum:", s print *, "elapsed wall clock time (seconds):", t1 - t0 end program
如何操作
我们的 C++和 Fortran 示例的CMakeLists.txt
将遵循一个在两种语言之间大体相似的模板:
- 两者都定义了最小 CMake 版本、项目名称和语言(
CXX
或Fortran
;我们将展示 C++版本):
cmake_minimum_required(VERSION 3.9 FATAL_ERROR) project(recipe-05 LANGUAGES CXX)
- 对于 C++示例,我们需要 C++11 标准:
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 两者都调用
find_package
来搜索 OpenMP:
find_package(OpenMP REQUIRED)
- 最后,我们定义可执行目标并链接到
FindOpenMP
模块提供的导入目标(在 Fortran 情况下,我们链接到OpenMP::OpenMP_Fortran
):
add_executable(example example.cpp) target_link_libraries(example PUBLIC OpenMP::OpenMP_CXX )
- 现在,我们可以配置并构建代码:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build .
- 首先让我们在并行环境下测试一下(本例中使用四个核心):
$ ./example 1000000000 number of available processors: 4 number of threads: 4 we will form sum of numbers from 1 to 1000000000 sum: 500000000500000000 elapsed wall clock time: 1.08343 seconds
- 为了比较,我们可以将示例重新运行,将 OpenMP 线程数设置为 1:
$ env OMP_NUM_THREADS=1 ./example 1000000000 number of available processors: 4 number of threads: 1 we will form sum of numbers from 1 to 1000000000 sum: 500000000500000000 elapsed wall clock time: 2.96427 seconds
CMake 秘籍(二)(4)https://developer.aliyun.com/article/1525091