CMake 秘籍(二)(3)

本文涉及的产品
简介: CMake 秘籍(二)

CMake 秘籍(二)(2)https://developer.aliyun.com/article/1525089

它是如何工作的

在这个 CMake 脚本中,有三个新的 CMake 命令:execute_processadd_custom_command,它们总是可用的,以及find_package_handle_standard_args,它需要include(FindPackageHandleStandardArgs)

execute_process命令将执行一个或多个作为当前发出的 CMake 命令的子进程的命令。最后一个子进程的返回值将被保存到作为参数传递给RESULT_VARIABLE的变量中,而标准输出和标准错误管道的内容将被保存到作为参数传递给OUTPUT_VARIABLEERROR_VARIABLE的变量中。execute_process允许我们执行任意命令,并使用它们的结果来推断我们系统的配置。在我们的例子中,我们首先使用它来确保 NumPy 可用,然后获取模块的版本。

find_package_handle_standard_args命令提供了处理与在给定系统上找到的程序和库相关的常见操作的标准工具。版本相关的选项,REQUIREDEXACT,在引用此命令时都得到了正确处理,无需进一步的 CMake 代码。额外的选项QUIETCOMPONENTS,我们很快就会遇到,也由这个 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_DSCALC_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.hppCxxLAPACK.cpp为 LAPACK 调用执行相应的翻译。

如何做到这一点

相应的CMakeLists.txt包含以下构建块:

  1. 我们定义了最小 CMake 版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-04 LANGUAGES CXX C Fortran)
  1. 我们要求使用 C++11 标准:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 此外,我们验证 Fortran 和 C/C++编译器是否能协同工作,并生成处理名称修饰的头部文件。这两项功能均由FortranCInterface模块提供:
include(FortranCInterface)
FortranCInterface_VERIFY(CXX)
FortranCInterface_HEADER(
 fc_mangle.h
 MACRO_NAMESPACE "FC_"
 SYMBOLS DSCAL DGESV
 )
  1. 然后,我们要求 CMake 查找 BLAS 和 LAPACK。这些是必需的依赖项:
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)
  1. 接下来,我们添加一个包含我们源代码的库,用于 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}
  )
  1. 注意,该目标的包含目录和链接库被声明为PUBLIC,因此任何依赖于数学库的额外目标也会在其包含目录中设置这些目录。
  2. 最后,我们添加一个可执行目标,并链接到math
add_executable(linear-algebra "")
target_sources(linear-algebra
  PRIVATE
    linear-algebra.cpp
  )
target_link_libraries(linear-algebra
  PRIVATE
    math
  )
  1. 在配置步骤中,我们可以专注于相关的输出:
$ 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.
...
  1. 最后,我们构建并测试可执行文件:
$ cmake --build .
$ ./linear-algebra 1000
C_DSCAL done
C_DGESV done
info is 0
check is 1.54284e-10

它是如何工作的

FindBLAS.cmakeFindLAPACK.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.hppCxxLAPACK.hpp中。为了使用FortranCInterface,我们不得不在LANGUAGES列表中添加 C 和 Fortran 支持。当然,我们可以定义自己的预处理器定义,但代价是有限的移植性。

我们将在第九章,混合语言项目中更详细地讨论 Fortran 和 C 的互操作性。

如今,许多 BLAS 和 LAPACK 的实现已经附带了一个围绕 Fortran 子程序的薄 C 层包装器。这些包装器多年来已经标准化,被称为 CBLAS 和 LAPACKE。

还有更多内容

许多数值代码严重依赖于矩阵代数操作,正确地链接到高性能的 BLAS 和 LAPACK API 实现非常重要。不同供应商在不同架构和并行环境下打包其库的方式存在很大差异。FindBLAS.cmakeFindLAPACK.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将遵循一个在两种语言之间大体相似的模板:

  1. 两者都定义了最小 CMake 版本、项目名称和语言(CXXFortran;我们将展示 C++版本):
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
project(recipe-05 LANGUAGES CXX)
  1. 对于 C++示例,我们需要 C++11 标准:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 两者都调用find_package来搜索 OpenMP:
find_package(OpenMP REQUIRED)
  1. 最后,我们定义可执行目标并链接到FindOpenMP模块提供的导入目标(在 Fortran 情况下,我们链接到OpenMP::OpenMP_Fortran):
add_executable(example example.cpp)
target_link_libraries(example
  PUBLIC
    OpenMP::OpenMP_CXX
  )
  1. 现在,我们可以配置并构建代码:
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
  1. 首先让我们在并行环境下测试一下(本例中使用四个核心):
$ ./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
  1. 为了比较,我们可以将示例重新运行,将 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

相关实践学习
基于函数计算一键部署掌上游戏机
本场景介绍如何使用阿里云计算服务命令快速搭建一个掌上游戏机。
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
相关文章
|
2月前
|
编译器 Shell
CMake 秘籍(八)(3)
CMake 秘籍(八)
24 2
|
2月前
|
编译器 Linux C语言
CMake 秘籍(二)(2)
CMake 秘籍(二)
27 2
|
2月前
|
消息中间件 Unix C语言
CMake 秘籍(二)(5)
CMake 秘籍(二)
22 1
|
2月前
|
Linux API iOS开发
CMake 秘籍(六)(1)
CMake 秘籍(六)
19 1
|
2月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
21 1
|
2月前
|
Linux 编译器 C++
CMake 秘籍(七)(2)
CMake 秘籍(七)
26 1
|
2月前
|
Linux C++ iOS开发
CMake 秘籍(三)(4)
CMake 秘籍(三)
13 1
|
2月前
|
Linux C++ iOS开发
CMake 秘籍(四)(1)
CMake 秘籍(四)
14 0
|
2月前
|
并行计算 Unix 编译器
CMake 秘籍(七)(5)
CMake 秘籍(七)
34 0
|
2月前
|
测试技术 C++
CMake 秘籍(四)(5)
CMake 秘籍(四)
15 0