
1. 为什么要在VS2017里折腾CLAPACK如果你正在用C或C做数值计算、信号处理、控制系统仿真或者搞一些跟矩阵运算沾边的算法验证大概率绕不开线性代数库。自己手写矩阵乘法、LU分解、特征值求解当然可以但性能和稳定性跟经过几十年打磨的成熟库完全不在一个量级。CLAPACK就是这样一个存在——它是经典线性代数库LAPACK的C语言接口版本底层还是那套久经考验的Fortran数值算法外面包了一层C调用约定让C/C开发者不用去啃Fortran的语法和编译规则就能直接调用。问题在于CLAPACK的官方发布包主要面向Unix-like环境在Windows上配合Visual Studio使用需要经过一系列编译、配置、链接的步骤。网上能搜到的教程要么年代久远要么语焉不详要么直接让你去下某个来路不明的预编译二进制包。我在一个实际项目中需要在VS2017环境下集成CLAPACK做矩阵运算前后折腾了差不多两天踩了不少坑最终跑通了一套稳定可复现的流程。这篇文章就是把整个过程完整记录下来包括每一步为什么这么做、参数怎么选、遇到问题怎么排查。这篇文章适合以下人群正在用VS2017做C/C开发、需要引入线性代数计算能力的工程师在校学生做课程设计或科研项目需要在Windows上快速搭建数值计算环境以及任何对CLAPACK感兴趣但被Windows编译配置卡住的人。我会从源码获取开始一步步走到最终在VS2017里调用CLAPACK函数完成矩阵求逆所有步骤都是我实际验证过的。2. 环境准备与源码获取2.1 确认基础环境在动手之前先把环境确认清楚。我用的配置是Windows 10 64位专业版、Visual Studio 2017 Community版本15.9.x、CLAPACK 3.2.1。VS2017的安装组件里必须勾选“使用C的桌面开发”工作负载其中要包含MSBuild、VC 2017工具集、Windows SDK这几项。如果你装的是VS2017的其他版本Professional或Enterprise操作完全一样不影响后续步骤。有一个容易被忽略的点CLAPACK的源码里包含大量Fortran文件编译过程中需要Fortran编译器。VS2017本身不带Fortran编译器所以我们需要用CLAPACK官方提供的f2c转换工具把Fortran源码预先转换成C源码。这个转换过程在CLAPACK的源码包里已经通过一个叫f2c的库完成了我们不需要额外安装Fortran编译器但需要确保源码包完整。注意不要试图用MinGW或Cygwin的gcc来编译CLAPACK然后在VS2017里链接运行库不兼容会导致一堆链接错误。全程用MSVC工具链保持一致。2.2 获取CLAPACK源码CLAPACK 3.2.1的源码可以从多个渠道获取。我推荐从官方历史归档站点下载clapack-3.2.1-CMAKE.tgz这个包注意是带CMAKE的那个版本不是早期的Makefile版本。带CMAKE的版本已经包含了CMake构建脚本在Windows上配合VS2017会方便很多。下载下来之后用7-Zip或WinRAR解压。解压后的目录结构大致是这样的clapack-3.2.1-CMAKE/ ├── BLAS/ │ ├── SRC/ │ └── ... ├── SRC/ │ ├── Makefile │ └── ... ├── F2CLIBS/ │ └── libf2c/ ├── CMAKE/ ├── CMakeLists.txt └── ...其中BLAS/SRC目录下是基础线性代数子程序的C源码SRC目录下是LAPACK主体函数的C源码F2CLIBS/libf2c是f2c运行库的源码。这三个部分最终会被编译成三个静态库blas.lib、lapack.lib、libf2c.lib或者合并成一个大的库看你怎么配置。提示网上有些教程让你直接下载clapack.lib和blas.lib的预编译版本我不建议这么做。预编译版本的编译器版本、运行库选项、优化参数都不可控跟你的VS2017项目很可能不匹配出了问题根本没法排查。从源码编译虽然多花二十分钟但一切尽在掌控。2.3 安装CMakeCMake是跨平台的构建工具CLAPACK的CMake版本需要用它来生成VS2017的解决方案文件。去CMake官网下载Windows安装包我用的版本是3.16.x这个版本对VS2017的支持很成熟。安装时选择“Add CMake to the system PATH for all users”这样在命令行里可以直接调用cmake命令。安装完成后打开命令提示符输入cmake --version确认安装成功。如果提示找不到命令检查一下PATH环境变量是否包含CMake的bin目录。3. 用CMake生成VS2017解决方案3.1 创建构建目录CMake的最佳实践是“源外构建”也就是不要在源码目录里直接生成构建文件而是单独建一个构建目录。这样做的好处是源码目录保持干净想重新构建或者换编译器时直接删掉构建目录就行不会污染源码。我在clapack-3.2.1-CMAKE同级目录下建了一个build文件夹D:\workspace\ ├── clapack-3.2.1-CMAKE\ └── build\然后打开命令提示符进入build目录cd /d D:\workspace\build3.2 执行CMake配置命令接下来执行CMake的配置命令指定源码目录、生成器、目标平台等参数cmake -G Visual Studio 15 2017 Win64 -DCMAKE_BUILD_TYPERelease -DUSE_OPTIMIZED_BLASON ..\clapack-3.2.1-CMAKE逐项解释一下这些参数的含义-G Visual Studio 15 2017 Win64指定生成器为VS2017的64位版本。VS2017的内部版本号是15所以生成器名称里写的是“Visual Studio 15 2017”。如果你要编译32位版本把“Win64”去掉即可。我强烈建议用64位因为数值计算场景下内存和性能都更充裕。-DCMAKE_BUILD_TYPERelease指定构建类型为Release。虽然VS2017是多配置生成器这个参数在生成阶段不完全生效但设置上没坏处后面在VS里编译时再选Release配置。-DUSE_OPTIMIZED_BLASON启用优化版的BLAS。CLAPACK自带的BLAS是参考实现性能一般。这个选项会尝试使用优化过的BLAS实现。不过在实际操作中这个选项有时候会引发一些链接问题如果你遇到奇怪的符号缺失可以先把它设为OFF用参考BLAS跑通再说。..\clapack-3.2.1-CMAKE源码目录的相对路径。执行完这条命令后CMake会开始检测编译器、检查依赖、生成项目文件。如果一切顺利最后会输出“Generating done”和“Build files have been written to: ...”的提示。注意如果CMake报错说找不到编译器检查一下VS2017是否安装完整以及是否在“Visual Studio 2017的开发人员命令提示符”里执行CMake。最稳妥的方式是从开始菜单打开“VS 2017的开发人员命令提示符”在里面执行CMake命令这样环境变量都是配好的。3.3 检查生成结果配置成功后build目录下会出现CLAPACK.sln解决方案文件以及一堆.vcxproj项目文件。用VS2017打开CLAPACK.sln在解决方案资源管理器里应该能看到多个项目主要包括blas基础线性代数子程序lapackLAPACK主体函数libf2cf2c运行库test相关的项目测试程序这时候先别急着编译有几个地方需要检查一下。4. 编译配置调整与静态库生成4.1 检查项目属性在VS2017里右键点击blas项目选择“属性”重点检查以下几项配置属性 - 常规 - 配置类型应该是“静态库(.lib)”。如果不是改成静态库。配置属性 - C/C - 代码生成 - 运行库这里很关键。如果你的最终项目是Release模式并且不想依赖VC运行库的DLL就选“多线程(/MT)”如果选“多线程DLL(/MD)”则最终可执行文件需要VC运行库的DLL支持。我一般选/MT这样生成的静态库可以独立使用部署时少一些依赖。但要注意你的最终项目也必须用相同的运行库选项否则会出现链接冲突。配置属性 - C/C - 优化 - 优化Release配置下选“最大优化(优选速度)(/O2)”。对lapack和libf2c项目做同样的检查。三个项目的运行库选项必须一致这是后面能顺利链接的前提。4.2 解决常见的编译错误在编译过程中我遇到了几个典型错误这里逐一说明解决方法。错误一f2c.h找不到这个错误通常是因为libf2c项目的包含目录没有配置好。右键libf2c项目 - 属性 - C/C - 常规 - 附加包含目录添加F2CLIBS/libf2c的路径。同时blas和lapack项目也需要包含这个目录因为它们编译时也会引用f2c.h。错误二error C2059: 语法错误:“,”这个错误一般出现在某些Fortran转换过来的C文件里原因是MSVC对某些宏展开的处理跟gcc不同。解决方法是在项目属性 - C/C - 预处理器 - 预处理器定义中添加_CRT_SECURE_NO_WARNINGS和NO_APPEND_FORTRAN。后者可以避免一些符号命名冲突。错误三链接时提示__imp__...未解析这是运行库选项不一致导致的。检查所有项目包括最终调用CLAPACK的项目是否都用了相同的运行库选项/MT或/MD。混用必然导致链接错误。4.3 执行编译配置检查完毕后在VS2017里选择“生成” - “批生成”勾选blas、lapack、libf2c三个项目的Release配置点击“生成”。编译过程大概需要几分钟取决于机器性能。编译成功后在build目录下的Release子目录里或者build/lib/Release取决于CMake的布局会生成blas.lib、lapack.lib、libf2c.lib三个静态库文件。把这三个.lib文件复制到一个单独的目录比如D:\workspace\clapack-libs\方便后续引用。提示如果你在编译lapack时遇到内存不足的错误比如fatal error C1060: 编译器的堆空间不足可以在项目属性 - C/C - 命令行 - 其他选项中添加/Zm200把编译器的内存上限调高。5. 在VS2017项目中集成CLAPACK5.1 创建测试项目新建一个VS2017的C控制台项目命名为ClapackTest。在项目属性里做以下配置C/C - 常规 - 附加包含目录添加CLAPACK源码中的头文件目录。具体来说需要包含clapack-3.2.1-CMAKE/INCLUDE和clapack-3.2.1-CMAKE/F2CLIBS/libf2c这两个目录。前者包含clapack.h和blaswrap.h后者包含f2c.h。链接器 - 常规 - 附加库目录添加刚才存放三个.lib文件的目录比如D:\workspace\clapack-libs\。链接器 - 输入 - 附加依赖项添加lapack.lib、blas.lib、libf2c.lib。注意顺序lapack依赖blas和libf2c所以lapack放在前面。C/C - 代码生成 - 运行库确保跟编译CLAPACK时选的选项一致我选的是/MT。5.2 编写测试代码下面是一段完整的测试代码功能是定义一个3x3矩阵调用CLAPACK的dgetrf和dgetri函数完成LU分解和矩阵求逆然后打印结果验证正确性。#include stdio.h #include stdlib.h #include clapack.h int main() { // 定义3x3矩阵按列主序存储CLAPACK使用列主序 double A[9] { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0 }; // 备份原始矩阵用于验证 double A_orig[9]; for (int i 0; i 9; i) A_orig[i] A[i]; // 矩阵维度 integer n 3; integer lda 3; integer info 0; // 用于存储LU分解的置换信息 integer ipiv[3]; // 第一步LU分解 dgetrf_(n, n, A, lda, ipiv, info); if (info ! 0) { printf(dgetrf failed, info %d\n, info); return -1; } // 第二步求逆 double work_query; integer lwork -1; dgetri_(n, A, lda, ipiv, work_query, lwork, info); lwork (integer)work_query; double *work (double *)malloc(lwork * sizeof(double)); dgetri_(n, A, lda, ipiv, work, lwork, info); if (info ! 0) { printf(dgetri failed, info %d\n, info); free(work); return -1; } // 打印逆矩阵 printf(Inverse matrix:\n); for (int i 0; i 3; i) { for (int j 0; j 3; j) { printf(%10.6f , A[i j * 3]); } printf(\n); } // 验证A * A_inv 应该接近单位矩阵 printf(\nVerification (A * A_inv):\n); for (int i 0; i 3; i) { for (int j 0; j 3; j) { double sum 0.0; for (int k 0; k 3; k) { sum A_orig[i k * 3] * A[k j * 3]; } printf(%10.6f , sum); } printf(\n); } free(work); return 0; }这段代码里有几个关键点需要说明。列主序存储CLAPACK沿用Fortran的列主序column-major存储方式而C语言默认是行主序。所以定义一个3x3矩阵时A[0]到A[2]是第一列A[3]到A[5]是第二列以此类推。这是初学者最容易搞错的地方矩阵元素位置放错会导致计算结果完全不对。函数名带下划线CLAPACK的函数名在C里通常带一个尾部下划线比如dgetrf_、dgetri_。这是因为Fortran编译器通常会在符号名后面加下划线f2c转换后保留了这个约定。如果你在链接时提示找不到dgetrf试试加下划线。integer类型CLAPACK定义了自己的integer类型通常是long或int具体取决于编译选项。在clapack.h里有定义直接用就行不要混用int。工作数组查询dgetri_需要一块工作数组大小通过lwork -1查询最优值。这是LAPACK的标准做法先查询再分配避免浪费或不足。5.3 编译运行把测试代码粘贴到main.cpp里编译。如果前面的配置都正确应该能顺利生成可执行文件。运行结果应该类似Inverse matrix: -0.666667 1.333333 -1.000000 0.666667 -3.666667 2.000000 -1.000000 2.000000 -1.000000 Verification (A * A_inv): 1.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 1.000000验证矩阵接近单位矩阵说明求逆计算正确。6. 常见问题排查与避坑经验6.1 链接错误速查表错误信息可能原因解决方法LNK2019: 无法解析的外部符号 _dgetrf_库文件未添加或顺序不对检查附加依赖项确保lapack.lib在blas.lib之前LNK2038: 检测到“RuntimeLibrary”的不匹配运行库选项不一致统一所有项目的/MT或/MD选项LNK2005: _malloc 已经在 libcmt.lib 中定义混用了不同运行库的库确保CLAPACK库和主项目用同一运行库C1083: 无法打开包括文件: “f2c.h”包含目录未配置添加F2CLIBS/libf2c到附加包含目录C2059: 语法错误预处理器定义缺失添加NO_APPEND_FORTRAN和_CRT_SECURE_NO_WARNINGS6.2 运行时问题程序崩溃在dgetrf_调用处最常见的原因是矩阵维度参数传错或者数组大小不够。检查n、lda是否与数组实际大小匹配。lda至少等于n如果矩阵是按列主序存储的n x n矩阵lda n。计算结果完全不对九成是列主序和行主序搞混了。记住CLAPACK用列主序C数组按列填充。如果你习惯行主序可以在调用前做转置或者干脆在代码里统一用列主序思维。内存泄漏dgetri_的工作数组用完后要free别漏了。如果在一个循环里反复调用每次都要释放。6.3 性能相关的经验CLAPACK默认编译出来的性能是“能用”级别不是“极致”级别。如果你对性能有要求有几个方向可以优化。第一编译时开启更高的优化等级。在项目属性 - C/C - 优化里选“最大优化(优选速度)(/O2)”并且开启“内联函数扩展”和“启用内部函数”。这些选项能让编译器生成更高效的SIMD指令。第二考虑链接外部优化BLAS。CLAPACK自带的参考BLAS性能一般如果你有Intel MKL或者OpenBLAS的Windows版本可以把BLAS替换掉。具体做法是编译CLAPACK时只编译lapack和libf2cBLAS用外部库替代。不过要注意函数命名约定和调用约定要匹配否则链接会出问题。第三矩阵规模较大时注意内存访问模式。CLAPACK的算法对缓存友好度一般如果矩阵超过缓存容量性能下降会比较明显。这种情况下可以考虑分块计算或者换用更现代的库。提示如果你只是做算法验证或者小规模计算CLAPACK的性能完全够用不用过度优化。把精力放在算法逻辑上更划算。6.4 关于32位和64位的选择我强烈建议用64位。原因有三第一64位下可用内存更大处理大矩阵时不容易爆内存第二64位下integer类型通常是64位能索引更大的数组第三现代Windows系统基本都是64位32位程序在64位系统上运行虽然兼容但性能没有优势。如果你确实需要32位版本把CMake生成器里的“Win64”去掉重新生成解决方案然后重新编译。注意32位和64位的.lib文件不能混用主项目也要对应改成32位。7. 实际项目中的集成建议7.1 封装成易用的C接口直接调用CLAPACK的C函数接口比较繁琐参数多、容易传错。在实际项目中我建议做一层薄薄的C封装把矩阵求逆、线性方程组求解、特征值计算等常用操作包装成类或函数。比如可以定义一个Matrix类内部用std::vectordouble存储数据构造时指定行数和列数提供inverse()、solve()等方法。在方法内部调用CLAPACK函数处理列主序转换、工作数组分配等细节。这样上层业务代码就不用关心CLAPACK的调用细节了。封装时要注意异常安全。CLAPACK函数通过info参数返回错误码封装层应该检查info非零时抛出异常或返回错误状态不要让错误静默传播。7.2 静态库 vs 动态库前面我们编译的是静态库.lib链接进最终可执行文件。这种方式部署简单不需要额外带DLL。缺点是每个用到CLAPACK的可执行文件都会包含一份库代码体积会大一些。如果你有多个项目共用CLAPACK或者希望减小可执行文件体积可以编译成动态库.dll。在CMake配置时把BUILD_SHARED_LIBS设为ON重新生成解决方案编译出来的就是DLL。使用DLL时主项目需要链接对应的导入库.lib运行时把DLL放在可执行文件同目录或系统PATH里。静态库和动态库各有优劣看你的部署场景。我个人的习惯是如果只有一个项目用静态库省事如果多个项目共用动态库更灵活。7.3 版本管理与团队协作CLAPACK的编译产物.lib文件、头文件应该纳入版本管理或者放在团队内部的制品库里。这样新加入的同事不用每个人都从头编译一遍直接拉取编译好的库文件就能用。同时把CMake配置命令、编译选项、运行库设置这些信息写进项目的README或者构建脚本里。我见过太多项目因为构建配置没有文档化换个人就编译不出来了。注意如果你把编译好的.lib文件提交到Git仓库注意文件大小。CLAPACK的静态库加起来可能有几十MBGit对大文件不友好。可以考虑用Git LFS或者把库文件放在单独的制品存储里。8. 一些补充说明和扩展思路CLAPACK虽然经典但毕竟年代久远接口设计偏底层。如果你是新项目也可以考虑一些更现代的替代方案。比如Eigen是纯头文件的C模板库接口优雅性能也不错在Windows上配合VS2017用起来很方便。Armadillo提供了类似MATLAB的语法底层可以切换不同的BLAS/LAPACK实现。如果你没有历史包袱这些库可能比CLAPACK更省心。但如果你已经有一个基于CLAPACK的项目或者需要跟已有的Fortran数值代码对接那CLAPACK仍然是可靠的选择。它的算法经过几十年验证数值稳定性有保障而且源码完全开放遇到问题可以深入到底层去排查。我在实际使用中的体会是CLAPACK在Windows上的编译配置确实有一定的门槛但一旦跑通后续使用就很稳定了。关键是把编译选项、运行库、链接顺序这几个点搞清楚剩下的就是查文档调用函数的事。希望这篇记录能帮你少走一些弯路。