第 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 会很有用。

目标

当工具链支持 IPO 时,为整个教程项目启用 IPO。

有用资源

要编辑的文件

  • CMakeLists.txt

入门

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

构建和运行

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

cmake --build build

如果 IPO 不可用,我们将在配置期间看到一条错误消息。否则,不会有任何变化。

注意

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

解决方案

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

TODO 6:点击显示/隐藏答案
TODO 6: MathFunctions/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_ 变量控制的项目范围行为的情况。