CMake 秘籍(二)(4)

简介: CMake 秘籍(二)

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 标准的实现包括以下内容:

  1. 运行时库。
  2. 头文件和 Fortran 90 模块。
  3. 编译器包装器,它调用用于构建 MPI 库的编译器,并带有额外的命令行参数来处理包含目录和库。通常,可用的编译器包装器包括mpic++/mpiCC/mpicxx用于 C++,mpicc用于 C,以及mpifort用于 Fortran。
  4. MPI 启动器:这是您应该调用的程序,用于启动编译代码的并行执行。其名称取决于实现,通常是以下之一:mpirunmpiexecorterun

本示例将展示如何在系统上找到合适的 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 模块:

  1. 首先,我们定义最小 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)
  1. 然后我们调用find_package来定位 MPI 实现:
find_package(MPI REQUIRED)
  1. 我们定义可执行文件的名称和源代码,并且与前面的示例类似,链接到导入的目标:
add_executable(hello-mpi hello-mpi.cpp)
target_link_libraries(hello-mpi
  PUBLIC
    MPI::MPI_CXX
  )
  1. 让我们配置并构建可执行文件:
$ 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 .
  1. 为了并行执行此程序,我们使用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 库:

  1. 我们首先声明 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)
  1. 我们还请求 OpenMP,因为 Eigen 可以利用共享内存并行性进行密集操作:
find_package(OpenMP REQUIRED)
  1. 我们通过调用find_packageCONFIG模式下搜索 Eigen(我们将在下一节讨论这一点):
find_package(Eigen3 3.3 REQUIRED CONFIG)
  1. 如果找到 Eigen,我们会打印出有帮助的状态消息。请注意,我们正在使用Eigen3::Eigen目标。正如我们在前两个配方中学到的,这是一个IMPORTED目标,由 Eigen 分发的原生 CMake 脚本提供:
if(TARGET Eigen3::Eigen)
  message(STATUS "Eigen3 v${EIGEN3_VERSION_STRING} found in ${EIGEN3_INCLUDE_DIR}")
endif()
  1. 接下来,我们为我们的源文件声明一个可执行目标:
add_executable(linear-algebra linear-algebra.cpp)
  1. 然后我们找到 BLAS。请注意,依赖项现在不是必需的:
find_package(BLAS)
  1. 如果找到 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()
  1. 最后,我们链接到导入的Eigen3::EigenOpenMP::OpenMP_CXX目标。这足以设置所有必要的编译和链接标志:
target_link_libraries(linear-algebra
  PUBLIC
    Eigen3::Eigen
    OpenMP::OpenMP_CXX
  )
  1. 我们现在已经准备好配置项目:
$ 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
  1. 最后,我们编译并测试代码。请注意,在这种情况下,二进制文件使用了四个可用线程:
$ 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.cmakeEigen3ConfigVersion.cmakeEigen3Targets.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 在哪里查找它:

  1. 通过传递 Eigen3 的安装前缀作为 CMAKE_PREFIX_PATH
$ cmake -D CMAKE_PREFIX_PATH=<installation-prefix> ..
  1. 通过传递配置文件的位置作为 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

相关文章
|
2月前
|
编译器 Linux C语言
CMake 秘籍(二)(2)
CMake 秘籍(二)
27 2
|
2月前
|
编译器 Shell 开发工具
CMake 秘籍(八)(5)
CMake 秘籍(八)
21 2
|
2月前
|
消息中间件 Unix C语言
CMake 秘籍(二)(5)
CMake 秘籍(二)
22 1
|
2月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
21 1
|
2月前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
18 1
|
2月前
|
Linux 测试技术 iOS开发
CMake 秘籍(四)(4)
CMake 秘籍(四)
15 0
|
2月前
|
Linux C++ iOS开发
CMake 秘籍(四)(1)
CMake 秘籍(四)
14 0
|
2月前
|
并行计算 Unix 编译器
CMake 秘籍(七)(5)
CMake 秘籍(七)
34 0
|
2月前
|
编译器 Linux 开发工具
CMake 秘籍(四)(2)
CMake 秘籍(四)
11 0
|
2月前
|
测试技术 C++
CMake 秘籍(四)(5)
CMake 秘籍(四)
15 0