3.7 检测Eigen库
NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-07 中找到,包含一个C++的示例。该示例在CMake 3.9版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-06 中也有一个适用于CMake 3.5的C++示例。
BLAS库为矩阵和向量操作提供了标准化接口。不过,这个接口用Fortran语言书写。虽然已经展示了如何使用C++直接使用这些库,但在现代C++程序中,希望有更高级的接口。
纯头文件实现的Eigen库,使用模板编程来提供接口。矩阵和向量的计算,会在编译时进行数据类型检查,以确保兼容所有维度的矩阵。密集和稀疏矩阵的运算,也可使用表达式模板高效的进行实现,如:矩阵-矩阵乘积,线性系统求解器和特征值问题。从3.3版开始,Eigen可以链接到BLAS和LAPACK库中,这可以将某些操作实现进行卸载,使库的实现更加灵活,从而获得更多的性能收益。
本示例将展示如何查找Eigen库,使用OpenMP并行化,并将部分工作转移到BLAS库。
本示例中会实现,矩阵-向量乘法和LU分解),可以选择卸载BLAS和LAPACK库中的一些实现。这个示例中,只考虑将在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。使用OpenMP将Eigen并行化,并从BLAS库中卸载部分线性代数实现:
首先声明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)
因为Eigen可以使用共享内存的方式,所以可以使用OpenMP并行处理计算密集型操作:
find_package(OpenMP REQUIRED)
调用
find_package
来搜索Eigen(将在下一小节中讨论):find_package(Eigen3 3.3 REQUIRED CONFIG)
如果找到Eigen,我们将打印状态信息。注意,使用的是
Eigen3::Eigen
,这是一个IMPORT
目标,可通过提供的CMake脚本找到这个目标:if(TARGET Eigen3::Eigen)
message(STATUS "Eigen3 v${EIGEN3_VERSION_STRING} found in ${EIGEN3_INCLUDE_DIR}")
endif()
接下来,将源文件声明为可执行目标:
add_executable(linear-algebra linear-algebra.cpp)
然后,找到BLAS。注意,现在不需要依赖项:
find_package(BLAS)
如果找到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()
最后,我们链接到
Eigen3::Eigen
和OpenMP::OpenMP_CXX
目标。这就可以设置所有必要的编译标示和链接标志:target_link_libraries(linear-algebra
PUBLIC
Eigen3::Eigen
OpenMP::OpenMP_CXX
)
开始配置:
$ 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
最后,编译并测试代码。注意,可执行文件使用四个线程运行:
$ 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查找,这样配置项目就会变得很容易。从3.3版开始,Eigen提供了CMake模块,这些模块将导出相应的目标Eigen3::Eigen
。
find_package
可以通过选项传递,届时CMake将不会使用FindEigen3.cmake
模块,而是通过特定的Eigen3Config.cmake
,Eigen3ConfigVersion.cmake
和Eigen3Targets.cmake
提供Eigen3安装的标准位置(<installation-prefix>/share/eigen3/cmake
)。这种包定位模式称为“Config”模式,比Find<package>.cmake
方式更加通用。有关“模块”模式和“配置”模式的更多信息,可参考官方文档 https://cmake.org/cmake/help/v3.5/command/find_package.html 。
虽然Eigen3、BLAS和OpenMP声明为PUBLIC
依赖项,但EIGEN_USE_BLAS
编译定义声明为PRIVATE
。可以在单独的库目标中汇集库依赖项,而不是直接链接可执行文件。使用PUBLIC/PRIVATE
关键字,可以根据库目标的依赖关系调整相应标志和定义。
更多信息
CMake将在预定义的位置层次结构中查找配置模块。首先是CMAKE_PREFIX_PATH
,<package>_DIR
是接下来的搜索路径。因此,如果Eigen3安装在非标准位置,可以使用这两个选项来告诉CMake在哪里查找它:
通过将Eigen3的安装前缀传递给
CMAKE_PREFIX_PATH
:$ cmake -D CMAKE_PREFIX_PATH=<installation-prefix> ..
通过传递配置文件的位置作为
Eigen3_DIR
:$ cmake -D Eigen3_DIR=<installation-prefix>/share/eigen3/cmake ..