# 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分解]([https://zh.wikipedia.org/wiki/LU%E5%88%86%E8%A7%A3](https://zh.wikipedia.org/wiki/LU分解)),可以選擇卸載BLAS和LAPACK庫中的一些實現。這個示例中,只考慮將在BLAS庫中卸載。
## 準備工作
本例中,我們編譯一個程序,該程序會從命令行獲取的隨機方陣和維向量。然后我們將用LU分解來解線性方程組**Ax=b**。以下是源代碼(` linear-algebra.cpp `):
```c++
#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庫中卸載部分線性代數實現:
1. 首先聲明CMake最低版本、項目名稱和使用C++11語言標準:
```cmake
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)
```
2. 因為Eigen可以使用共享內存的方式,所以可以使用OpenMP并行處理計算密集型操作:
```cmake
find_package(OpenMP REQUIRED)
```
3. 調用`find_package`來搜索Eigen(將在下一小節中討論):
```cmake
find_package(Eigen3 3.3 REQUIRED CONFIG)
```
4. 如果找到Eigen,我們將打印狀態信息。注意,使用的是`Eigen3::Eigen`,這是一個`IMPORT`目標,可通過提供的CMake腳本找到這個目標:
```cmake
if(TARGET Eigen3::Eigen)
message(STATUS "Eigen3 v${EIGEN3_VERSION_STRING} found in ${EIGEN3_INCLUDE_DIR}")
endif()
```
5. 接下來,將源文件聲明為可執行目標:
```cmake
add_executable(linear-algebra linear-algebra.cpp)
```
6. 然后,找到BLAS。注意,現在不需要依賴項:
```cmake
find_package(BLAS)
```
7. 如果找到BLAS,我們可為可執行目標,設置相應的宏定義和鏈接庫:
```cmake
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()
```
8. 最后,我們鏈接到`Eigen3::Eigen`和`OpenMP::OpenMP_CXX`目標。這就可以設置所有必要的編譯標示和鏈接標志:
```cmake
target_link_libraries(linear-algebra
PUBLIC
Eigen3::Eigen
OpenMP::OpenMP_CXX
)
```
9. 開始配置:
```shell
$ 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
```
10. 最后,編譯并測試代碼。注意,可執行文件使用四個線程運行:
```shell
$ 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在哪里查找它:
1. 通過將Eigen3的安裝前綴傳遞給`CMAKE_PREFIX_PATH`:
```shell
$ cmake -D CMAKE_PREFIX_PATH=<installation-prefix> ..
```
2. 通過傳遞配置文件的位置作為`Eigen3_DIR`:
```shell
$ cmake -D Eigen3_DIR=<installation-prefix>/share/eigen3/cmake ..
```
- Introduction
- 前言
- 第0章 配置環境
- 0.1 獲取代碼
- 0.2 Docker鏡像
- 0.3 安裝必要的軟件
- 0.4 測試環境
- 0.5 上報問題并提出改進建議
- 第1章 從可執行文件到庫
- 1.1 將單個源文件編譯為可執行文件
- 1.2 切換生成器
- 1.3 構建和鏈接靜態庫和動態庫
- 1.4 用條件句控制編譯
- 1.5 向用戶顯示選項
- 1.6 指定編譯器
- 1.7 切換構建類型
- 1.8 設置編譯器選項
- 1.9 為語言設定標準
- 1.10 使用控制流
- 第2章 檢測環境
- 2.1 檢測操作系統
- 2.2 處理與平臺相關的源代碼
- 2.3 處理與編譯器相關的源代碼
- 2.4 檢測處理器體系結構
- 2.5 檢測處理器指令集
- 2.6 為Eigen庫使能向量化
- 第3章 檢測外部庫和程序
- 3.1 檢測Python解釋器
- 3.2 檢測Python庫
- 3.3 檢測Python模塊和包
- 3.4 檢測BLAS和LAPACK數學庫
- 3.5 檢測OpenMP的并行環境
- 3.6 檢測MPI的并行環境
- 3.7 檢測Eigen庫
- 3.8 檢測Boost庫
- 3.9 檢測外部庫:Ⅰ. 使用pkg-config
- 3.10 檢測外部庫:Ⅱ. 自定義find模塊
- 第4章 創建和運行測試
- 4.1 創建一個簡單的單元測試
- 4.2 使用Catch2庫進行單元測試
- 4.3 使用Google Test庫進行單元測試
- 4.4 使用Boost Test進行單元測試
- 4.5 使用動態分析來檢測內存缺陷
- 4.6 預期測試失敗
- 4.7 使用超時測試運行時間過長的測試
- 4.8 并行測試
- 4.9 運行測試子集
- 4.10 使用測試固件
- 第5章 配置時和構建時的操作
- 5.1 使用平臺無關的文件操作
- 5.2 配置時運行自定義命令
- 5.3 構建時運行自定義命令:Ⅰ. 使用add_custom_command
- 5.4 構建時運行自定義命令:Ⅱ. 使用add_custom_target
- 5.5 構建時為特定目標運行自定義命令
- 5.6 探究編譯和鏈接命令
- 5.7 探究編譯器標志命令
- 5.8 探究可執行命令
- 5.9 使用生成器表達式微調配置和編譯
- 第6章 生成源碼
- 6.1 配置時生成源碼
- 6.2 使用Python在配置時生成源碼
- 6.3 構建時使用Python生成源碼
- 6.4 記錄項目版本信息以便報告
- 6.5 從文件中記錄項目版本
- 6.6 配置時記錄Git Hash值
- 6.7 構建時記錄Git Hash值
- 第7章 構建項目
- 7.1 使用函數和宏重用代碼
- 7.2 將CMake源代碼分成模塊
- 7.3 編寫函數來測試和設置編譯器標志
- 7.4 用指定參數定義函數或宏
- 7.5 重新定義函數和宏
- 7.6 使用廢棄函數、宏和變量
- 7.7 add_subdirectory的限定范圍
- 7.8 使用target_sources避免全局變量
- 7.9 組織Fortran項目
- 第8章 超級構建模式
- 8.1 使用超級構建模式
- 8.2 使用超級構建管理依賴項:Ⅰ.Boost庫
- 8.3 使用超級構建管理依賴項:Ⅱ.FFTW庫
- 8.4 使用超級構建管理依賴項:Ⅲ.Google Test框架
- 8.5 使用超級構建支持項目
- 第9章 語言混合項目
- 9.1 使用C/C++庫構建Fortran項目
- 9.2 使用Fortran庫構建C/C++項目
- 9.3 使用Cython構建C++和Python項目
- 9.4 使用Boost.Python構建C++和Python項目
- 9.5 使用pybind11構建C++和Python項目
- 9.6 使用Python CFFI混合C,C++,Fortran和Python
- 第10章 編寫安裝程序
- 10.1 安裝項目
- 10.2 生成輸出頭文件
- 10.3 輸出目標
- 10.4 安裝超級構建
- 第11章 打包項目
- 11.1 生成源代碼和二進制包
- 11.2 通過PyPI發布使用CMake/pybind11構建的C++/Python項目
- 11.3 通過PyPI發布使用CMake/CFFI構建C/Fortran/Python項目
- 11.4 以Conda包的形式發布一個簡單的項目
- 11.5 將Conda包作為依賴項發布給項目
- 第12章 構建文檔
- 12.1 使用Doxygen構建文檔
- 12.2 使用Sphinx構建文檔
- 12.3 結合Doxygen和Sphinx
- 第13章 選擇生成器和交叉編譯
- 13.1 使用CMake構建Visual Studio 2017項目
- 13.2 交叉編譯hello world示例
- 13.3 使用OpenMP并行化交叉編譯Windows二進制文件
- 第14章 測試面板
- 14.1 將測試部署到CDash
- 14.2 CDash顯示測試覆蓋率
- 14.3 使用AddressSanifier向CDash報告內存缺陷
- 14.4 使用ThreadSaniiser向CDash報告數據爭用
- 第15章 使用CMake構建已有項目
- 15.1 如何開始遷移項目
- 15.2 生成文件并編寫平臺檢查
- 15.3 檢測所需的鏈接和依賴關系
- 15.4 復制編譯標志
- 15.5 移植測試
- 15.6 移植安裝目標
- 15.7 進一步遷移的措施
- 15.8 項目轉換為CMake的常見問題
- 第16章 可能感興趣的書
- 16.1 留下評論——讓其他讀者知道你的想法