CMake 秘籍(二)(1)https://developer.aliyun.com/article/1525088
如何操作
根据 Eigen 库的文档,只需设置适当的编译器标志即可启用向量化代码的生成。让我们看看CMakeLists.txt
:
- 我们声明一个 C++11 项目:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-06 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 由于我们希望使用 Eigen 库,因此我们需要在系统上找到其头文件:
find_package(Eigen3 3.3 REQUIRED CONFIG)
- 我们包含
CheckCXXCompilerFlag.cmake
标准模块文件:
include(CheckCXXCompilerFlag)
- 我们检查
-march=native
编译器标志是否有效:
check_cxx_compiler_flag("-march=native" _march_native_works)
- 我们还检查了替代的
-xHost
编译器标志:
check_cxx_compiler_flag("-xHost" _xhost_works)
- 我们设置一个空变量
_CXX_FLAGS
,以保存我们刚刚检查的两个标志中找到的一个有效标志。如果我们看到_march_native_works
,我们将_CXX_FLAGS
设置为-march=native
。如果我们看到_xhost_works
,我们将_CXX_FLAGS
设置为-xHost
。如果两者都不起作用,我们将保持_CXX_FLAGS
为空,向量化将被禁用:
set(_CXX_FLAGS) if(_march_native_works) message(STATUS "Using processor's vector instructions (-march=native compiler flag set)") set(_CXX_FLAGS "-march=native") elseif(_xhost_works) message(STATUS "Using processor's vector instructions (-xHost compiler flag set)") set(_CXX_FLAGS "-xHost") else() message(STATUS "No suitable compiler flag found for vectorization") endif()
- 为了进行比较,我们还为未优化的版本定义了一个可执行目标,其中我们不使用前面的优化标志:
add_executable(linear-algebra-unoptimized linear-algebra.cpp) target_link_libraries(linear-algebra-unoptimized PRIVATE Eigen3::Eigen )
- 此外,我们还定义了一个优化版本:
add_executable(linear-algebra linear-algebra.cpp) target_compile_options(linear-algebra PRIVATE ${_CXX_FLAGS} ) target_link_libraries(linear-algebra PRIVATE Eigen3::Eigen )
- 让我们比较这两个可执行文件——首先我们进行配置(在这种情况下,
-march=native_works
):
$ mkdir -p build $ cd build $ cmake .. ... -- Performing Test _march_native_works -- Performing Test _march_native_works - Success
-- Performing Test _xhost_works -- Performing Test _xhost_works - Failed -- Using processor's vector instructions (-march=native compiler flag set) ...
- 最后,让我们编译并比较时间:
$ cmake --build . $ ./linear-algebra-unoptimized result: -261.505 elapsed seconds: 1.97964 $ ./linear-algebra result: -261.505 elapsed seconds: 1.05048
工作原理
大多数现代处理器提供向量指令集。精心编写的代码可以利用这些指令集,并在与非向量化代码相比时实现增强的性能。Eigen 库在编写时就明确考虑了向量化,因为线性代数操作可以从中大大受益。我们所需要做的就是指示编译器为我们检查处理器,并为当前架构生成原生指令集。不同的编译器供应商使用不同的标志来实现这一点:GNU 编译器通过-march=native
标志实现这一点,而 Intel 编译器使用-xHost
标志。然后我们使用CheckCXXCompilerFlag.cmake
模块提供的check_cxx_compiler_flag
函数:
check_cxx_compiler_flag("-march=native" _march_native_works)
该函数接受两个参数:第一个是要检查的编译器标志,第二个是用于存储检查结果的变量,即true
或false
。如果检查结果为正,我们将工作标志添加到_CXX_FLAGS
变量中,然后该变量将用于设置我们可执行目标的编译器标志。
还有更多
这个配方可以与之前的配方结合使用;可以使用cmake_host_system_information
查询处理器能力。
第四章:检测外部库和程序
在本章中,我们将涵盖以下食谱:
- 检测 Python 解释器
- 检测 Python 库
- 检测 Python 模块和包
- 检测 BLAS 和 LAPACK 数学库
- 检测 OpenMP 并行环境
- 检测 MPI 并行环境
- 检测 Eigen 库
- 检测 Boost 库
- 检测外部库:I. 使用
pkg-config
- 检测外部库:II. 编写一个查找模块
引言
项目通常依赖于其他项目和库。本章演示了如何检测外部库、框架和项目以及如何链接到这些。CMake 有一个相当广泛的预打包模块集,用于检测最常用的库和程序,例如 Python 和 Boost。你可以使用cmake --help-module-list
获取现有模块的列表。然而,并非所有库和程序都被覆盖,有时你将不得不提供自己的检测脚本。在本章中,我们将讨论必要的工具并发现 CMake 命令的查找家族:
find_file
来查找一个指定文件的完整路径find_library
来查找一个库find_package
来查找并加载来自外部项目的设置find_path
来查找包含指定文件的目录find_program
来查找一个程序
你可以使用--help-command
命令行开关来打印任何 CMake 内置命令的文档到屏幕上。
检测 Python 解释器
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-01
找到。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
Python 是一种非常流行的动态语言。许多项目将用 Python 编写的工具与它们的主程序和库一起打包,或者在配置或构建过程中使用 Python 脚本。在这种情况下,确保运行时依赖于 Python 解释器也得到满足是很重要的。本食谱将展示如何在配置步骤中检测和使用 Python 解释器。我们将介绍find_package
命令,该命令将在本章中广泛使用。
如何操作
我们将逐步构建CMakeLists.txt
文件:
- 我们首先定义最小 CMake 版本和项目名称。请注意,对于这个例子,我们将不需要任何语言支持:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-01 LANGUAGES NONE)
- 然后,我们使用
find_package
命令来查找 Python 解释器:
find_package(PythonInterp REQUIRED)
- 接着,我们执行一个 Python 命令并捕获其输出和返回值:
execute_process( COMMAND ${PYTHON_EXECUTABLE} "-c" "print('Hello, world!')" RESULT_VARIABLE _status OUTPUT_VARIABLE _hello_world ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE )
- 最后,我们打印 Python 命令的返回值和输出:
message(STATUS "RESULT_VARIABLE is: ${_status}") message(STATUS "OUTPUT_VARIABLE is: ${_hello_world}")
- 现在,我们可以检查配置步骤的输出:
$ mkdir -p build $ cd build $ cmake .. -- Found PythonInterp: /usr/bin/python (found version "3.6.5") -- RESULT_VARIABLE is: 0 -- OUTPUT_VARIABLE is: Hello, world! -- Configuring done -- Generating done -- Build files have been written to: /home/user/cmake-cookbook/chapter-03/recipe-01/example/build
它是如何工作的
find_package
是 CMake 模块的包装命令,用于发现和设置软件包。这些模块包含用于在系统上的标准位置识别软件包的 CMake 命令。CMake 模块的文件称为Find.cmake
,当发出find_package()
调用时,它们包含的命令将在内部运行。
除了实际在系统上发现请求的软件包之外,查找模块还设置了一组有用的变量,反映实际找到的内容,可以在自己的CMakeLists.txt
中使用。对于 Python 解释器,相关模块是FindPythonInterp.cmake
,随 CMake 一起提供,并设置以下变量:
PYTHONINTERP_FOUND
,一个布尔值,表示是否找到了解释器PYTHON_EXECUTABLE
,Python 解释器可执行文件的路径PYTHON_VERSION_STRING
,Python 解释器的完整版本号PYTHON_VERSION_MAJOR
,Python 解释器的主版本号PYTHON_VERSION_MINOR
,Python 解释器的小版本号PYTHON_VERSION_PATCH
,Python 解释器的补丁号
可以强制 CMake 查找特定版本的软件包。例如,使用此方法请求 Python 解释器的版本大于或等于 2.7:
find_package(PythonInterp 2.7)
也可以强制要求满足依赖关系:
find_package(PythonInterp REQUIRED)
在这种情况下,如果在常规查找位置找不到适合的 Python 解释器可执行文件,CMake 将中止配置。
CMake 有许多用于查找广泛使用的软件包的模块。我们建议始终在 CMake 在线文档中搜索现有的Find.cmake
模块,并在使用它们之前阅读其文档。find_package
命令的文档可以在cmake.org/cmake/help/v3.5/command/find_package.html
找到。在线文档的一个很好的替代方法是浏览github.com/Kitware/CMake/tree/master/Modules
中的 CMake 模块源代码 - 它们的标题文档说明了模块使用的变量以及模块设置的变量,可以在自己的CMakeLists.txt
中使用。
还有更多
有时,软件包未安装在标准位置,CMake 可能无法正确找到它们。可以使用 CLI 开关-D
告诉 CMake 在特定位置查找特定软件以传递适当的选项。对于 Python 解释器,可以按以下方式配置:
$ cmake -D PYTHON_EXECUTABLE=/custom/location/python ..
这将正确识别安装在非标准/custom/location/python
目录中的 Python 可执行文件。
每个包都不同,Find.cmake
模块试图考虑到这一点并提供统一的检测接口。当系统上安装的包无法被 CMake 找到时,我们建议您阅读相应检测模块的文档,以了解如何正确指导 CMake。您可以直接在终端中浏览文档,例如使用cmake --help-module FindPythonInterp
。
无论检测包的情况如何,我们都想提到一个方便的打印变量的辅助模块。在本食谱中,我们使用了以下内容:
message(STATUS "RESULT_VARIABLE is: ${_status}") message(STATUS "OUTPUT_VARIABLE is: ${_hello_world}")
调试的一个便捷替代方法是使用以下内容:
include(CMakePrintHelpers) cmake_print_variables(_status _hello_world)
这将产生以下输出:
-- _status="0" ; _hello_world="Hello, world!"
关于打印属性和变量的便捷宏的更多文档,请参见cmake.org/cmake/help/v3.5/module/CMakePrintHelpers.html
。
检测 Python 库
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-02
找到,包含一个 C 语言示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
如今,使用 Python 工具分析和操作编译程序的输出已经非常普遍。然而,还有其他更强大的方法将解释型语言(如 Python)与编译型语言(如 C 或 C++)结合。一种方法是通过提供新的类型和在这些类型上的新功能来扩展Python,通过将 C 或 C++模块编译成共享库。这将是第九章,混合语言项目中食谱的主题。另一种方法是嵌入Python 解释器到一个 C 或 C++程序中。这两种方法都需要以下内容:
- 一个可用的 Python 解释器版本
- 可用的 Python 头文件
Python.h
- Python 运行时库
libpython
这三个组件必须锁定到完全相同的版本。我们已经演示了如何找到 Python 解释器;在本食谱中,我们将展示如何找到成功嵌入所需的两个缺失成分。
准备工作
我们将使用 Python 文档页面上找到的一个简单的 Python 嵌入到 C 程序的示例。源文件名为hello-embedded-python.c
:
#include <Python.h> int main(int argc, char *argv[]) { Py_SetProgramName(argv[0]); /* optional but recommended */ Py_Initialize(); PyRun_SimpleString("from time import time,ctime\n" "print 'Today is',ctime(time())\n"); Py_Finalize(); return 0; }
这些代码示例将在程序中初始化 Python 解释器的一个实例,并使用 Python 的time
模块打印日期。
嵌入示例代码可以在 Python 文档页面上在线找到,网址为docs.python.org/2/extending/embedding.html
和docs.python.org/3/extending/embedding.html
。
如何操作
在我们的CMakeLists.txt
中,需要遵循以下步骤:
- 第一块包含最小 CMake 版本、项目名称和所需语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-02 LANGUAGES C)
- 在本食谱中,我们强制使用 C99 标准进行 C 语言编程。这严格来说不是链接 Python 所必需的,但可能是您想要设置的东西:
set(CMAKE_C_STANDARD 99) set(CMAKE_C_EXTENSIONS OFF) set(CMAKE_C_STANDARD_REQUIRED ON)
- 找到 Python 解释器。现在这是一个必需的依赖项:
find_package(PythonInterp REQUIRED)
- 找到 Python 头文件和库。适当的模块称为
FindPythonLibs.cmake
:
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
- 我们添加一个使用
hello-embedded-python.c
源文件的可执行目标:
add_executable(hello-embedded-python hello-embedded-python.c)
- 可执行文件包含
Python.h
头文件。因此,此目标的包含目录必须包含 Python 包含目录,可通过PYTHON_INCLUDE_DIRS
变量访问:
target_include_directories(hello-embedded-python PRIVATE ${PYTHON_INCLUDE_DIRS} )
- 最后,我们将可执行文件链接到 Python 库,通过
PYTHON_LIBRARIES
变量访问:
target_link_libraries(hello-embedded-python PRIVATE ${PYTHON_LIBRARIES} )
- 现在,我们准备运行配置步骤:
$ mkdir -p build $ cd build $ cmake .. ... -- Found PythonInterp: /usr/bin/python (found version "3.6.5") -- Found PythonLibs: /usr/lib/libpython3.6m.so (found suitable exact version "3.6.5")
- 最后,我们执行构建步骤并运行可执行文件:
$ cmake --build . $ ./hello-embedded-python Today is Thu Jun 7 22:26:02 2018
它是如何工作的
FindPythonLibs.cmake
模块将在标准位置查找 Python 头文件和库。由于这些是我们项目的必需依赖项,如果找不到这些依赖项,配置将停止并出现错误。
请注意,我们明确要求 CMake 检测 Python 可执行文件的安装。这是为了确保可执行文件、头文件和库具有匹配的版本。这对于确保运行时不会出现版本不匹配导致的崩溃至关重要。我们通过使用FindPythonInterp.cmake
中定义的PYTHON_VERSION_MAJOR
和PYTHON_VERSION_MINOR
实现了这一点:
find_package(PythonInterp REQUIRED) find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
使用EXACT
关键字,我们已约束 CMake 检测特定且在这种情况下匹配的 Python 包含文件和库版本。为了更精确匹配,我们可以使用精确的PYTHON_VERSION_STRING
:
find_package(PythonInterp REQUIRED) find_package(PythonLibs ${PYTHON_VERSION_STRING} EXACT REQUIRED)
还有更多
我们如何确保即使 Python 头文件和库不在标准安装目录中,它们也能被正确找到?对于 Python 解释器,可以通过将PYTHON_LIBRARY
和PYTHON_INCLUDE_DIR
选项通过-D
选项传递给 CLI 来强制 CMake 在特定目录中查找。这些选项指定以下内容:
PYTHON_LIBRARY
,Python 库的路径PYTHON_INCLUDE_DIR
,Python.h
所在的路径
这确保将选择所需的 Python 版本。
有时需要将-D PYTHON_EXECUTABLE
、-D PYTHON_LIBRARY
和-D PYTHON_INCLUDE_DIR
传递给 CMake CLI,以便找到所有必需的组件并将它们固定到完全相同的版本。
另请参见
要精确匹配 Python 解释器及其开发组件的版本可能非常困难。这在它们安装在非标准位置或系统上安装了多个版本的情况下尤其如此。CMake 在其版本 3.12 中添加了新的 Python 检测模块,旨在解决这个棘手的问题。我们的CMakeLists.txt
中的检测部分也将大大简化:
find_package(Python COMPONENTS Interpreter Development REQUIRED)
我们鼓励您阅读新模块的文档:cmake.org/cmake/help/v3.12/module/FindPython.html
检测 Python 模块和包
本配方的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-03
找到,并包含一个 C++示例。本配方适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
在前一个配方中,我们展示了如何检测 Python 解释器以及如何编译一个简单的 C 程序,嵌入 Python 解释器。这两项任务是结合 Python 和编译语言时的基础。通常,你的代码会依赖于特定的 Python 模块,无论是 Python 工具、嵌入 Python 的编译程序,还是扩展它的库。例如,NumPy 在涉及矩阵代数的问题中在科学界变得非常流行。在依赖于 Python 模块或包的项目中,确保这些 Python 模块的依赖得到满足是很重要的。本配方将展示如何探测用户的环境以找到特定的 Python 模块和包。
准备工作
我们将在 C++程序中尝试一个稍微更复杂的嵌入示例。该示例再次取自 Python 在线文档(docs.python.org/3.5/extending/embedding.html#pure-embedding
),并展示了如何通过调用编译的 C++可执行文件来执行用户定义的 Python 模块中的函数。
Python 3 示例代码(Py3-pure-embedding.cpp
)包含以下源代码(有关相应的 Python 2 等效内容,请参见docs.python.org/2/extending/embedding.html#pure-embedding
):
#include <Python.h> int main(int argc, char *argv[]) { PyObject *pName, *pModule, *pDict, *pFunc; PyObject *pArgs, *pValue; int i; if (argc < 3) { fprintf(stderr, "Usage: pure-embedding pythonfile funcname [args]\n"); return 1; } Py_Initialize(); PyRun_SimpleString("import sys"); PyRun_SimpleString("sys.path.append(\".\")"); pName = PyUnicode_DecodeFSDefault(argv[1]); /* Error checking of pName left out */ pModule = PyImport_Import(pName); Py_DECREF(pName); if (pModule != NULL) { pFunc = PyObject_GetAttrString(pModule, argv[2]); /* pFunc is a new reference */ if (pFunc && PyCallable_Check(pFunc)) { pArgs = PyTuple_New(argc - 3); for (i = 0; i < argc - 3; ++i) { pValue = PyLong_FromLong(atoi(argv[i + 3])); if (!pValue) { Py_DECREF(pArgs); Py_DECREF(pModule); fprintf(stderr, "Cannot convert argument\n"); return 1; } /* pValue reference stolen here: */ PyTuple_SetItem(pArgs, i, pValue); } pValue = PyObject_CallObject(pFunc, pArgs); Py_DECREF(pArgs); if (pValue != NULL) { printf("Result of call: %ld\n", PyLong_AsLong(pValue)); Py_DECREF(pValue); } else { Py_DECREF(pFunc); Py_DECREF(pModule); PyErr_Print(); fprintf(stderr, "Call failed\n"); return 1; } } else { if (PyErr_Occurred()) PyErr_Print(); fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]); } Py_XDECREF(pFunc); Py_DECREF(pModule); } else { PyErr_Print(); fprintf(stderr, "Failed to load \"%s\"\n", argv[1]); return 1; } Py_Finalize(); return 0; }
我们希望嵌入的 Python 代码(use_numpy.py
)使用 NumPy 设置一个矩阵,其中所有矩阵元素都设置为 1.0:
import numpy as np def print_ones(rows, cols): A = np.ones(shape=(rows, cols), dtype=float) print(A) # we return the number of elements to verify # that the C++ code is able to receive return values num_elements = rows*cols return(num_elements)
如何操作
在下面的代码中,我们希望使用 CMake 检查 NumPy 是否可用。首先,我们需要确保 Python 解释器、头文件和库都在我们的系统上可用。然后,我们将继续确保 NumPy 可用:
- 首先,我们定义最小 CMake 版本、项目名称、语言和 C++标准:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-03 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 找到解释器、头文件和库的过程与之前的脚本完全相同:
find_package(PythonInterp REQUIRED) find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)
- 正确打包的 Python 模块知道它们的安装位置和版本。这可以通过执行一个最小的 Python 脚本来探测。我们可以在
CMakeLists.txt
内部执行这一步骤:
execute_process( COMMAND ${PYTHON_EXECUTABLE} "-c" "import re, numpy; print(re.compile('/__init__.py.*').sub('',numpy.__file__))" RESULT_VARIABLE _numpy_status OUTPUT_VARIABLE _numpy_location ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE )
_numpy_status
变量将在找到 NumPy 时为整数,否则为带有某些错误消息的字符串,而_numpy_location
将包含 NumPy 模块的路径。如果找到 NumPy,我们将其位置保存到一个简单的名为NumPy
的新变量中。请注意,新变量被缓存;这意味着 CMake 创建了一个持久变量,用户可以稍后修改它:
if(NOT _numpy_status) set(NumPy ${_numpy_location} CACHE STRING "Location of NumPy") endif()
- 下一步是检查模块的版本。再次,我们在
CMakeLists.txt
中部署一些 Python 魔法,将版本保存到一个_numpy_version
变量中:
execute_process( COMMAND ${PYTHON_EXECUTABLE} "-c" "import numpy; print(numpy.__version__)" OUTPUT_VARIABLE _numpy_version ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE )
- 最后,我们让
FindPackageHandleStandardArgs
CMake 包设置NumPy_FOUND
变量并以正确格式输出状态信息:
include(FindPackageHandleStandardArgs) find_package_handle_standard_args(NumPy FOUND_VAR NumPy_FOUND REQUIRED_VARS NumPy VERSION_VAR _numpy_version )
- 一旦所有依赖项都被正确找到,我们就可以编译可执行文件并将其链接到 Python 库:
add_executable(pure-embedding "") target_sources(pure-embedding PRIVATE Py${PYTHON_VERSION_MAJOR}-pure-embedding.cpp ) target_include_directories(pure-embedding PRIVATE ${PYTHON_INCLUDE_DIRS} ) target_link_libraries(pure-embedding PRIVATE ${PYTHON_LIBRARIES} )
- 我们还必须确保
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 ) # make sure building pure-embedding triggers the above custom command target_sources(pure-embedding PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py )
- 现在,我们可以测试检测和嵌入代码:
$ mkdir -p build $ cd build $ cmake .. -- ... -- Found PythonInterp: /usr/bin/python (found version "3.6.5") -- Found PythonLibs: /usr/lib/libpython3.6m.so (found suitable exact version "3.6.5") -- Found NumPy: /usr/lib/python3.6/site-packages/numpy (found version "1.14.3") $ cmake --build . $ ./pure-embedding use_numpy print_ones 2 3 [[1\. 1\. 1.] [1\. 1\. 1.]] Result of call: 6
CMake 秘籍(二)(3)https://developer.aliyun.com/article/1525090