第 6 步:深入系统内省

为了发现有关系统环境和工具链的信息,CMake 通常会编译一些小的测试程序,以验证编译器标志、头文件、内置函数或其他语言构造的可用性。

在这一步中,我们将利用 CMake 在我们自己的项目代码中使用的相同测试程序机制。

背景

追溯到配置和构建系统最早期的古老技巧,就是通过编译一个使用某项功能的小程序来验证该功能是否可用。

CMake 使这种做法在许多情况下变得不必要。正如我们将在后续步骤中提到的,如果 CMake 可以找到一个库依赖项,我们就可以依赖它具有我们期望的所有设施(头文件、代码生成器、测试工具等)。相反,如果 CMake 找不到依赖项,尝试使用该依赖项几乎肯定会失败。

然而,还有其他一些关于工具链的信息是 CMake 无法直接传达的。对于这些高级情况,我们可以编写自己的测试程序和编译命令来检查其可用性。

CMake 提供了用于简化这些检查的模块。这些模块记录在 cmake-modules(7) 中。任何以 Check 开头的模块都是我们可以用来询问工具链和系统环境的系统内省模块。一些值得注意的模块包括:

CheckIncludeFiles

检查一个或多个 C/C++ 头文件。

CheckCompilerFlag

检查编译器是否支持给定的标志。

CheckSourceCompiles

检查是否可以为给定语言构建源代码。

CheckIPOSupported

检查编译器是否支持过程间优化(IPO/LTO)。

练习 1 - 检查包含文件

一种快速且简单的检查方法是检查特定平台上是否存在给定的头文件,为此 CMake 提供了 CheckIncludeFiles。这最适用于系统和内在头文件,这些头文件可能不由特定包提供,但在许多构建环境中预期是可用的。

include(CheckIncludeFiles)
check_include_files(sys/socket.h HAVE_SYS_SOCKET_H LANGUAGE CXX)

注意

这些函数在 CMake 中不是立即生效的,必须通过 include() 其关联模块(即 CMakeLang 文件)来添加。许多模块位于 CMake 自身的 Modules 文件夹中。这个内置的 Modules 文件夹是 CMake 在评估 include() 命令时搜索的地方之一。你可以将这些模块视为标准库头文件,它们预期是可用的。

一旦已知头文件存在,我们就可以使用已经介绍过的条件判断和目标命令的相同机制将其传达给我们的代码。

目标

检查 x86 SSE2 内在头文件是否可用,如果可用,则使用它来改进 mathfunctions::sqrt

有用资源

要编辑的文件

  • MathFunctions/CMakeLists.txt

  • MathFunctions/MathFunctions.cxx

开始

Help/guide/tutorial/Step6 目录包含了对 Step5 的完整推荐解决方案,以及针对此步骤的相关 TODO。它还包含针对各种条件的 sqrt 函数的专门实现,你可以在 MathFunctions/MathFunctions.cxx 中找到它们。

完成 TODO 1TODO 3。请注意,库中已经添加了一些 #ifdef 指令,随着我们进行此步骤,这些指令将改变其操作。

构建并运行

我们可以使用常用的命令进行配置。

cmake --preset tutorial
cmake --build build

在配置步骤的输出中,我们应该观察到 CMake 正在检查 emmintrin.h 头文件。

-- Looking for include file emmintrin.h
-- Looking for include file emmintrin.h - found

如果你的系统上有此头文件,请验证 Tutorial 的输出是否包含关于使用 SSE2 的消息。相反,如果头文件不可用,你应该看到来自 Tutorial 的常规行为。

解决方案

首先,我们包含并使用 CheckIncludeFiles 模块,验证 emmintrin.h 头文件是否可用。

TODO 1:点击显示/隐藏答案
TODO 1: MathFunctions/CMakeLists.txt
include(CheckIncludeFiles)
check_include_files(emmintrin.h HAS_EMMINTRIN LANGUAGE CXX)

然后,我们使用检查结果有条件地在 MathFunctions 上设置编译定义。

TODO 2:点击显示/隐藏答案
TODO 2: MathFunctions/CMakeLists.txt
if(HAS_EMMINTRIN)
  target_compile_definitions(MathFunctions PRIVATE TUTORIAL_USE_SSE2)
endif()

最后,我们可以有条件地在 MathFunctions 库中包含该头文件。

TODO 3:点击显示/隐藏答案
TODO 3: MathFunctions/MathFunctions.cxx
#ifdef TUTORIAL_USE_SSE2
#  include <emmintrin.h>
#endif

练习 2 - 检查源代码编译

有时仅检查头文件是不够的。当没有头文件可供检查时(例如编译器内置函数的情况),尤其如此。对于这些场景,我们有 CheckSourceCompiles

include(CheckSourceCompiles)
check_source_compiles(CXX
  "
    int main() {
      int a, b, c;
      __builtin_add_overflow(a, b, &c);
    }
  "
  HAS_CHECKED_ADDITION
)

注意

默认情况下,CheckSourceCompiles 会构建并链接一个可执行文件。要检查的代码必须提供一个有效的 int main() 才能成功。

执行检查后,此系统内省的应用方式与我们讨论过的头文件检查方式相同。

目标

检查 GNU SSE2 内置函数是否可用,如果可用,则使用它们来改进 mathfunctions::sqrt

有用资源

要编辑的文件

  • MathFunctions/CMakeLists.txt

开始

完成 TODO 4TODO 5。不需要对 MathFunctions 的实现进行代码更改,因为这些已经提供了。

构建并运行

我们只需要重新构建教程即可。

cmake --build build

注意

如果检查失败而你认为它应该成功,你需要通过删除 CMakeCache.txt 文件来清除 CMake 缓存。如果 CMake 已经有了缓存结果,它将不会在后续运行中重新执行编译检查。

在配置步骤的输出中,我们应该观察到 CMake 正在检查提供的源代码是否编译成功,这将在我们提供给 check_source_compiles() 的变量名下报告。

-- Performing Test HAS_GNU_BUILTIN
-- Performing Test HAS_GNU_BUILTIN - Success

如果你的编译器上有这些内置函数,请验证 Tutorial 的输出是否包含关于使用 GNU 内置函数的消息。相反,如果这些内置函数不可用,你应该看到来自 Tutorial 的之前行为。

解决方案

首先,我们包含并使用 CheckSourceCompiles 模块,验证提供的源代码是否可以被构建。

TODO 4:点击显示/隐藏答案
TODO 4: MathFunctions/CMakeLists.txt
include(CheckSourceCompiles)
check_source_compiles(CXX
  [=[
    typedef double v2df __attribute__((vector_size(16)));
    int main() {
      __builtin_ia32_sqrtsd(v2df{});
    }
  ]=]
  HAS_GNU_BUILTIN
)

然后,我们使用检查结果有条件地在 MathFunctions 上设置编译定义。

TODO 5:点击显示/隐藏答案
TODO 5: MathFunctions/CMakeLists.txt
if(HAS_GNU_BUILTIN)
  target_compile_definitions(MathFunctions PRIVATE TUTORIAL_USE_GNU_BUILTIN)
endif()

练习 3 - 检查过程间优化

过程间优化和链接时优化可以为某些软件提供显著的性能提升。CMake 有能力通过 CheckIPOSupported 检查 IPO 标志的可用性。

include(CheckIPOSupported)
check_ipo_supported() # fatal error if IPO is not supported
set_target_properties(MyApp
  PROPERTIES
    INTERPROCEDURAL_OPTIMIZATION TRUE
)

注意

关于项目内的 IPO 配置,有几个重要的注意事项:

  • CMake 并不了解每个编译器上的每个 IPO/LTO 标志,通常通过针对已知工具链进行单独调整可以获得更好的结果。

  • 在目标上设置 INTERPROCEDURAL_OPTIMIZATION 属性不会改变它所链接的任何目标,也不会改变来自其他项目的依赖项。IPO 只能“看到”同样被适当编译的其他目标。

由于这些原因,应该认真考虑通过外部机制(预设、-D 标志、工具链文件 等)而不是项目内控制,在整个依赖树的所有项目中手动设置 IPO/LTO 标志。

然而,特别是对于超大型项目,有一个在项目内机制在可用时使用 IPO 是有用的。

目标

在整个教程项目中,当工具链可用时启用 IPO。

有用资源

要编辑的文件

  • CMakeLists.txt

入门

继续编辑 Step6 中的文件。完成 TODO 6TODO 7

构建和运行

我们只需要重新构建教程即可。

cmake --build build

如果 IPO 不可用,我们将在配置过程中看到错误消息。否则,没有任何变化。

注意

无论 IPO 检查的结果如何,我们都不应该期望 TutorialMathFunctions 的行为有任何变化。

解决方案

第一个 TODO 很简单,我们向项目中添加另一个选项。

TODO 6:点击显示/隐藏答案
TODO 6: CMakeLists.txt
option(TUTORIAL_ENABLE_IPO "Check for and use IPO support" ON)

下一步比较复杂,但 CheckIPOSupported 的文档中几乎有一个完整的示例,说明我们需要做什么。唯一的区别是我们将在整个项目范围内而不是为单个目标启用 IPO。

TODO 7:点击显示/隐藏答案
TODO 7: CMakeLists.txt
if(TUTORIAL_ENABLE_IPO)
  include(CheckIPOSupported)
  check_ipo_supported(RESULT result OUTPUT output)
  if(result)
    message("IPO is supported, enabling IPO")
    set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
  else()
    message(WARNING "IPO is not supported: ${output}")
  endif()
endif()

注意

通常,我们不鼓励在项目内部设置 CMAKE_ 变量。在这里,我们通过 option() 控制该行为。这允许打包者选择退出我们的覆盖。对于我们想要提供选项来控制受 CMAKE_ 变量控制的项目范围行为的情况,这是一种不完美但可以接受的解决方案。