BLAS 库为矩阵和向量操作提供了标准化接口,不过这个接口是用 Fortran 语言书写的。虽然已经展示过如何在 C++ 中直接使用这些库,但在现代 C++ 程序中,我们希望有更高级的接口。
纯头文件实现的 Eigen 库使用模板编程来提供接口:矩阵和向量的计算会在编译时进行数据类型检查,以确保兼容所有维度的矩阵;密集和稀疏矩阵的运算也可以通过表达式模板高效地实现,例如矩阵-矩阵乘积、线性系统求解器和特征值问题。从 3.3 版开始,Eigen 可以链接到 BLAS 和 LAPACK 库,把某些操作的实现卸载出去,使库的使用更加灵活,从而获得更多的性能收益。
本示例将展示如何查找 Eigen 库,使用 OpenMP 将其并行化,并把部分工作转移到 BLAS 库。示例会实现矩阵-向量乘法和 LU 分解,可以选择卸载 BLAS 和 LAPACK 库中的一些实现;本示例只考虑卸载到 BLAS 库的情况。
注意:本示例包含一个 C++ 示例,在 CMake 3.9 或更高版本中有效,并已在 GNU/Linux、macOS 和 Windows 上测试通过。
具体步骤
本例中,我们编译一个程序,该程序从命令行获取方阵的维度,生成随机方阵和随机向量,然后用 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 库。
工作原理
Eigen 支持通过 CMake 查找,这样配置项目就会变得很容易。从 3.3 版开始,Eigen 提供了 CMake 模块,这些模块将导出相应的目标 Eigen3::Eigen。
可以在调用 find_package 时传递选项,此时 CMake 将不会使用 FindEigen3.cmake 模块,而是使用 Eigen3 在标准安装位置(<installation-prefix>/share/eigen3/cmake)提供的 Eigen3Config.cmake、Eigen3ConfigVersion.cmake 和 Eigen3Targets.cmake。这种包定位模式称为 “Config” 模式,比 Find<package>.cmake 方式更通用。
虽然 Eigen3、BLAS 和 OpenMP 声明为 PUBLIC 依赖项,但 EIGEN_USE_BLAS 编译定义声明为 PRIVATE。可以在单独的库目标中汇集库依赖项,而不是直接链接可执行文件;使用 PUBLIC/PRIVATE 关键字,可以根据库目标的依赖关系调整相应的标志和定义。
CMake 将在预定义的位置层次结构中查找配置模块:首先是 CMAKE_PREFIX_PATH,接下来是 <package>_DIR。因此,如果 Eigen3 安装在非标准位置,可以使用这两个选项告诉 CMake 在哪里查找。