尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VS配置LAPACK:OpenBLAS+LAPACKE免编译支持32/64位

VS配置LAPACK:OpenBLAS+LAPACKE免编译支持32/64位 上个月一个做结构分析的朋友半夜发消息给我说他的 VS 工程要解一个几百阶的稠密线性方程组手写高斯消元精度顶不住想上 LAPACK结果折腾两天卡在链接那一步——要么是“无法解析的外部符号 sgesv_”要么是编译过了跑起来弹出“找不到 openblas.dll”。这种场面我见过太多次。LAPACK 在 Linux 下就是一行apt install liblapack-dev的事到了 Windows Visual Studio 这套环境因为牵扯 Fortran 编译器、BLAS 后端、符号命名约定、32/64 位 ABI 这一串问题门槛突然就竖起来了。这篇聊的是我这些年最省事的一条路不装 gfortran、不碰 CMake、不自己编译直接拿现成的预编译库把 64 位和 32 位的 VS 的 C 工程都配好 LAPACK。适合两类人一是刚接触数值计算、被链接错误劝退的入门同学二是手上有老工程必须同时出 32 位和 64 位版本、又不想维护两套构建脚本的老手。1. 概念先理清LAPACK、BLAS、LAPACKE 谁是谁动手之前把这三者的关系捋顺后面所有配置都是顺理成章的不然改属性表就是盲改。1.1 LAPACK 负责高层算法BLAS 负责底层计算LAPACK 是用来解线性代数问题的库做的是 LU 分解、Cholesky 分解、QR 分解、特征值、奇异值分解这类“高层”算法。它自己并不实现矩阵乘法的底层循环而是把矩阵乘加这类操作甩给 BLAS。所以任何一个可用的 LAPACK 背后都必然拖着一个 BLAS 实现。BLAS 的实现有好几种参考实现Netlib 那份慢但正确、OpenBLAS用汇编重写了热点 kernel快很多、Intel MKL针对自家 CPU 深度优化。这里有个关键信息很多人不知道OpenBLAS 内部已经集成了一份完整的 LAPACK 实现装完 OpenBLAS 等于同时拿到了 BLAS 和 LAPACK不用再单独去找 LAPACK 库。这一点直接决定了后面选型的思路。1.2 LAPACKE 是给 C/C 用的那层封装LAPACK 本身是 Fortran 写的原始接口长这样dgesv(n, nrhs, a, lda, ipiv, b, ldb, info)所有参数按引用传递矩阵按列主序存储。从 C 里直接调它要面对三件事函数名修饰规则不同编译器可能变成dgesv_、_dgesv_、DGEsv之类、传参要传地址、下标从 1 开始。LAPACKE 就是官方提供的 C 包装层函数名统一成LAPACKE_dgesv参数直接传值还能通过一个matrix_layout参数告诉它你的数据是行主序还是列主序。我的建议是能在 C 里就用 LAPACKE 接口别去碰 Fortran 原始符号能省掉一大半符号名相关的坑。1.3 为什么“自己编译”这条路我不想走网上一搜“Windows 编译 LAPACK”出来的教程清一色是装 MinGW-w64 或 MSYS2、装 gfortran、下 CMake、改CMakeLists.txt、打开-DBUILD_SHARED_LIBS、调到吐。这套流程在小规模测试里能跑通但有几个现实问题参考实现那份 Fortran 代码用不同版本的 gfortran 编译出来的符号名后缀规则可能不一致多一个下划线少一个下划线的事要同时出 32 位和 64 位就得准备两套 Fortran 工具链32 位的 MinGW 还得单独装编译产物一大堆.a/.lib/.dll手动挑哪几个要链进去很容易挑错更麻烦的是运行时库匹配Fortran 编译器默认可能用/MT而你的工程是/MD一混就是 LNK2038 或者运行时崩溃。算下来自己编译唯一的好处是可控代价是几小时起步的折腾和一堆说不清的坑。2. 三条现成路子怎么选不自己编译能走的路其实就那么几条我把它们摆在一起对比一下你按自己情况挑。2.1 三条路的横向对比路线上手难度64 位32 位需要额外工具链典型体积适用场景vcpkg 安装 OpenBLAS低支持支持只需 Git几十 MB想一次配好、长期维护的工程官方预编译二进制包低支持部分版本提供无几十 MB不想装任何包管理器、手工接库Intel oneAPI MKL中支持支持ia32 目标需装 oneAPI数百 MB 起追求极致性能、已用 Intel 生态表格里的“需要额外工具链”这一列是关键。第一、二条路都不需要 Fortran 编译器因为库是别人拿现成工具链编好的第三条需要装 oneAPI 安装器但它装的是应用层不是 Fortran 编译链而且 MKL 的 LAPACKE 接口也是现成的。2.2 我对选型的一点判断如果这是新工程或者你手上项目没有历史包袱我基本都推荐第一条路原因很简单跨机器迁移的时候别人只要装个 vcpkg敲一行命令就能把环境复现出来不用你写文档告诉他“去哪个页面下哪个 zip”。如果是那种交付给客户的老工程客户机器上不允许装额外软件那第二条路更合适把include、lib、dll三个目录塞进工程目录一起带走就行。MKL 那条路我一般只在两种情况下用一是对性能有硬要求比如大规模稠密矩阵分解二是工程本来就依赖 Intel 的数学库多它一个不多。3. 方案一vcpkg 一条命令拉齐 64 位与 32 位这是我最常用的方式整个流程大概十分钟。3.1 vcpkg 装到哪、怎么起先把仓库拉下来。位置有讲究路径里不要有中文、不要有空格。我见过有人放在C:\Users\张三\我的文档\下面结果脚本一层层解析路径时直接爆掉。git clone https://github.com/microsoft/vcpkg.git D:/dev/vcpkg cd D:/dev/vcpkg bootstrap-vcpkg.batbootstrap-vcpkg.bat会下载一个预编译的 vcpkg 可执行文件不用你自己编。跑完之后当前目录下会有vcpkg.exe。这一步如果卡在下载上是网络问题多试几次或者换个时间段。装好之后可以先vcpkg version确认一下能跑。3.2 装哪个包为什么是 OpenBLAS 而不是 lapack这里有个坑要说清楚。vcpkg 里确实存在一个叫lapack的端口但那个端口本质是lapack-reference也就是 Netlib 的参考实现编译它需要 Fortran 编译器vcpkg 会顺手给你拉一个 MinGW 的 gfortran 下来——这就跟“不自己编译”的初衷对着干了而且参考实现性能很差。所以正确选择是装 OpenBLASvcpkg install openblas:x64-windows vcpkg install openblas:x86-windows第一条出 64 位第二条出 32 位。如果你想要静态链接不想带着 DLL 跑把三元组换成x64-windows-static和x86-windows-static这样链进去的是一个.lib运行时不需要额外拷 DLL。两种三元组的取舍动态库工程体积小、升级方便但要保证目标机器上有对应 DLL静态库部署简单但可执行文件会大一圈而且如果同时链了别的库运行时库/MT 还是 /MD必须统一不然就是 LNK2038。三条三元组的含义梳理一下三元组位宽链接方式运行时库备注x64-windows64 位动态/MD默认最常用x64-windows-static64 位静态/MT部署省心体积大x86-windows32 位动态/MD必须在 x86 平台配置下用3.3 把库接进 VS 工程的两种姿势姿势一全局集成。执行vcpkg integrate install之后你在这台机器上新建或打开的任何 VS 工程都会自动把 vcpkg 的包含目录和库目录塞进工程属性里附加依赖项也会自动补上。缺点是全局生效换台机器又得重来一遍团队协作时别人不知道你依赖了它。姿势二manifest 模式。在工程目录下建一个vcpkg.json声明依赖{ name: my-lapack-demo, version: 1.0.0, dependencies: [ { name: openblas, default-features: true } ] }然后在工程属性里把“vcpkg”那栏打开选择“使用 vcpkg 清单”。这样做的好处是依赖被记录在仓库里谁 clone 下来都能还原。缺点是第一次构建会较慢vcpkg 要按 manifest 重新装依赖。我更偏向姿势二因为它把“配了什么库”这件事变成了代码的一部分而不是某台机器上的环境状态。4. 方案二拿现成的预编译二进制自己接有些场景不方便装包管理器比如交付环境或者封闭内网。4.1 从官方发布页拿 Windows 包OpenBLAS 的发布页会提供 Windows 版的压缩包通常按x64、x86、x64_64这类后缀区分还会分“带 LAPACKE”和“不带”的版本。一定要选带 LAPACKE 的那个命名里通常能看到lapacke字样。解压出来一般是这样的结构bin放 DLLlib放导入库include放头文件。这里有个必须提醒的点不同版本、不同发布者打包方式差异很大。有的给的是.libMSVC 用有的给的是.dll.a或者.aMinGW 用MSVC 工程必须用.lib那种。另外导入库的名字有时是libopenblas.lib有时是openblas.lib附加依赖项里写的名字必须跟实际文件名一字不差。下载后先双击进lib目录看清楚文件名再动手别凭记忆写。4.2 Intel oneAPI MKL 这条线如果你已经装了 oneAPI那 MKL 里的 LAPACKE 也是现成的。头文件是mkl.h或者单独用lapacke.h库目录下有按架构分好的子目录比如intel64和ia32前者对应 64 位后者对应 32 位。用 MKL 的时候要注意 LP64 和 ILP64 的区别也就是整数索引到底是 32 位还是 64 位选错了会在矩阵规模大的时候悄悄出问题。一般工程用 LP64整数是 32 位就行跟 LAPACKE 默认的lapack_int一致。4.3 目录怎么摆为后面切换平台铺路手工接库最容易乱的就是目录。我一般这么放third_party/ openblas/ include/ lapacke.h, lapack.h 等头文件 lib/ x64/ x64 用的 .lib x86/ x86 用的 .lib bin/ x64/ x64 用的 .dll x86/ x86 用的 .dll为什么按位宽分而不是按库名分因为 VS 的属性表支持按平台切换路径头文件是共用的不用分但.lib和.dll必须分。这样摆好之后x64 配置引用lib/x64x86 配置引用lib/x86属性表里改一个宏就能切换后面维护成本极低。5. VS 工程属性逐项配置实操不管走哪条路最后都要落到工程属性上。5.1 必须动的三处包含目录、库目录、附加依赖项右键工程 → 属性注意右上角的“配置”和“平台”这两个下拉框决定了你改的是哪套配置。很多新手改完 x64 的配置以为万事大吉切到 Release 或者 x86 就报错就是没注意这里。第一处C/C → 常规 → 附加包含目录填third_party/openblas/includevcpkg 的话填installed/x64-windows/include。第二处链接器 → 常规 → 附加库目录填lib/x64那个目录。第三处链接器 → 输入 → 附加依赖项填libopenblas.lib。三处都填完链接期才能找到符号。还有一处容易被忽略C/C → 代码生成 → 运行时库。用 vcpkg 的动态三元组这里必须是/MD或/MDd用静态三元组这里必须是/MT或/MTd。跟库不一致的话就算链过去了运行时也可能崩在奇怪的地方。5.2 用属性表把两个平台彻底隔开每次切换平台都去改三处属性太累用属性表.props一次写好。在“属性管理器”里新建两张表一张叫openblas-x64.props一张叫openblas-x86.props?xml version1.0 encodingutf-8? Project ToolsVersion4.0 xmlnshttp://schemas.microsoft.com/developer/msbuild/2003 ItemDefinitionGroup Condition$(Platform)x64 ClCompile AdditionalIncludeDirectories$(SolutionDir)third_party\openblas\include;%(AdditionalIncludeDirectories)/AdditionalIncludeDirectories /ClCompile Link AdditionalLibraryDirectories$(SolutionDir)third_party\openblas\lib\x64;%(AdditionalLibraryDirectories)/AdditionalLibraryDirectories AdditionalDependencieslibopenblas.lib;%(AdditionalDependencies)/AdditionalDependencies /Link /ItemDefinitionGroup ItemDefinitionGroup Condition$(Platform)Win32 ClCompile AdditionalIncludeDirectories$(SolutionDir)third_party\openblas\include;%(AdditionalIncludeDirectories)/AdditionalIncludeDirectories /ClCompile Link AdditionalLibraryDirectories$(SolutionDir)third_party\openblas\lib\x86;%(AdditionalLibraryDirectories)/AdditionalLibraryDirectories AdditionalDependencieslibopenblas.lib;%(AdditionalDependencies)/AdditionalDependencies /Link /ItemDefinitionGroup /Project注意两个细节一是 32 位在 VS 里叫Win32不是x86条件判断写错就不生效二是路径用了$(SolutionDir)这样整个解决方案目录搬走也不会失效。两张表分别挂到对应配置上之后切换平台就不用再手动改了。5.3 DLL 放哪儿才不会运行时翻车用动态库的话编译链接都过了只是第一步运行时还得能找到 DLL。openblas.dll的搜索路径是可执行文件所在目录 → 系统目录 → PATH 里的目录。最稳的做法是每次构建后自动拷到输出目录。在工程属性 → 生成事件 → 后期生成事件里写命令行xcopy /Y /D $(SolutionDir)third_party\openblas\bin\$(Platform)\*.dll $(OutDir)这里$(Platform)是x64或Win32所以你的bin目录名要跟平台名对上不然拷不到。如果嫌麻烦也可以直接把 DLL 放到$(OutDir)手动维护一份但那样一旦更新库版本容易忘记同步。我个人更推荐生成事件一次配置长期有效。6. 跑通一个最小例子解二元一次方程组配置完总得验证一下。用一个简单的例子求解2*x1 1*x2 3 1*x1 3*x2 5手算的话系数矩阵行列式是 23-115所以 x1 (33-15)/5 0.8x2 (25-13)/5 1.4。6.1 完整代码#include stdio.h #include lapacke.h int main(void) { /* 系数矩阵 A按行主序摆第一行 2 1第二行 1 3 */ double a[4] { 2.0, 1.0, 1.0, 3.0 }; /* 右端项 b解算完会被原地覆盖为 x */ double b[2] { 3.0, 5.0 }; /* 主元索引长度等于 n */ lapack_int ipiv[2] { 0, 0 }; lapack_int n 2; /* 方程阶数 */ lapack_int nrhs 1; /* 右端列数 */ lapack_int lda n; /* 行主序下 A 的 leading dimension */ lapack_int ldb nrhs; /* 行主序下 B 的 leading dimension */ lapack_int info 0; /* 第三个参数 LAPACK_ROW_MAJOR 告诉库我们的数据是行主序 */ info LAPACKE_dgesv(LAPACK_ROW_MAJOR, n, nrhs, a, lda, ipiv, b, ldb); if (info 0) { printf(x1 %.6f\n, b[0]); printf(x2 %.6f\n, b[1]); } else if (info 0) { printf(矩阵奇异U(%d,%d) 为零无法求解\n, (int)info, (int)info); } else { printf(第 %d 个参数非法\n, (int)-info); } return 0; }编译运行输出应该是x1 0.800000、x2 1.400000。如果结果对得上说明从包含目录到链接到运行时 DLL 整条链路都通了。6.2 LAPACKE_dgesv 的参数逐个数第一个参数是matrix_layout取值LAPACK_ROW_MAJOR或LAPACK_COL_MAJOR这是 LAPACKE 相比 Fortran 原始接口最贴心的地方。第二个n是矩阵阶数因为是方阵行数列数相同。第三个nrhs是右端项的列数我这里只有一个未知量组所以是 1。第四个a是系数矩阵注意它会被分解结果覆盖原矩阵就没了需要保留原值的话提前拷贝一份。第五个lda是“主维数”行主序下等于列数也就是 n。第六个ipiv是主元索引数组长度至少为 n输出用。第七个b进来是右端项、出去是解原地覆盖。第八个ldb是 b 的主维数行主序下等于 nrhs。返回值info的语义要记住0 表示成功正数表示第几个主元为零矩阵奇异负数表示第几个参数非法。6.3 列主序这个大坑上面代码里我特意用了LAPACK_ROW_MAJOR但真实工程里更常见的是别人给你的数据是按列主序存的这时候如果你声明成行主序算出来的结果会错得离谱而且不报错排查起来极其痛苦。判断方法很简单看你的双层循环如果内层下标是行号就是列主序如果内层下标是列号就是行主序。用LAPACK_ROW_MAJOR的时候 LAPACKE 内部会帮你做转置处理代价是有一点额外拷贝开销如果你自己就是按列主序存的直接声明LAPACK_COL_MAJOR此时lda等于行数性能也最好。我一般建议新写的代码统一按列主序组织声明也用 COL_MAJOR跟 LAPACK 的天然习惯一致不用来回转。7. 报错速查与排查思路配库过程中的错误来来回回就那么几类整理成表遇到了直接对号入座。7.1 链接阶段报错常见原因处理办法LNK2019 无法解析LAPACKE_dgesv附加依赖项没填或填错名字核对lib目录里.lib的实际文件名LNK2019 无法解析dgesv_直接调了 Fortran 原始接口符号名对不上改用 LAPACKE 接口或者查清下划线后缀规则LNK2038RuntimeLibrary不匹配库用 /MT工程用 /MD 或反过来把两边运行时库选项统一LNK1104 无法打开xxx.lib库目录路径错、文件名错、缺少分号用绝对路径试一次确认文件确实存在LNK1112 计算机类型冲突32 位的 lib 链进了 64 位配置检查库目录是否按平台分开链接错误排查的核心思路是先确认文件在不在再确认名字对不对最后确认位宽和运行时库匹不匹配。三步走完基本都能定位。7.2 运行阶段“找不到 xxx.dll”这类报错八成是 DLL 没进可执行文件目录。用后期生成事件拷一份或者临时把 DLL 所在的bin目录加进系统 PATH。更隐蔽的一种是0xc000007b这个错误码基本就是 32 位和 64 位 DLL 混用导致的你的 exe 是 64 位PATH 里却有个 32 位的同名 DLL 被优先加载了。排查手段是用依赖查看工具看 exe 实际加载了哪个路径下的 DLL对比位宽。还有一种不报错但结果不对的情况多半是列主序和行主序搞反或者lda、ldb填错了。lda填小了会读越界填大了不会报错但会浪费空间甚至算错。我自己的习惯是在调用前把n、lda、ldb打印出来对一遍特别是从别人的示例代码改过来的时候。7.3 32 位与 64 位混用的典型症状最后一个要格外小心的细节LAPACKE 里的lapack_int类型位宽是跟着构建配置走的。如果你在头文件里传的是int但库是按 64 位整数索引ILP64编译的参数在栈上就会错位结果是崩溃或者算出垃圾值。解决办法是老老实实用lapack_int声明所有跟库交互的整数变量不要图省事用int。另外 32 位进程的地址空间只有 2GB 左右解大规模问题时即使是同样阶数的矩阵32 位版本也比 64 位版本更容易内存分配失败这一点在做双版本测试时会明显感觉到。8. 这些年踩过的坑与几条实在建议配 LAPACK 这事难点从来不在 LAPACK 本身而在“几套工具链的边界对齐”。我把几条踩出来的经验摆出来能帮你少走点弯路。第一优先用 LAPACKE 而不是 Fortran 原始接口。原始接口的符号名规则跟编译器、位宽、下划线后缀强相关同一个函数在 32 位和 64 位下可能名字都不一样而 LAPACKE 的符号名是稳定的。这一条能省掉最多的链接期折磨。第二别把 MKL 和 OpenBLAS 混着链。两个库都提供了 BLAS 和 LAPACK 符号同时链进去链接器会先匹配到先出现的那个另一个的符号可能就解析串了。真要用 MKL就把 OpenBLAS 的附加依赖项撤掉二选一。第三属性表要进版本库。.props是纯文本跟源码一起提交团队里其他人拉下来就有一致的配置。我见过太多“在我机器上能编”的项目最后都是因为工程属性靠手工点击设置、没人记录。第四32 位版本只在必须出的时候维护。如果你的目标平台没有硬性 32 位需求我建议只出 64 位。维护两套配置、两套库、两轮测试成本是实打实的而 32 位进程的内存上限会让大规模计算直接撞墙。第五验证环节别省。库配好之后一定跑一遍第 6 节那种有解析解的小例子结果对上了再往工程里接业务代码。不然真出问题的时候你分不清是自己的算法写错了还是库链接串了排查范围直接翻倍。第六也是我吃过亏的一条换机器或者升级 VS 之后先确认 vcpkg 的三元组还在不在。VS 大版本升级有时会把 vcpkg 的集成状态重置这时候附加依赖项看起来还在但实际路径失效报一堆莫名的链接错误。跑一遍vcpkg integrate install通常就好了。配库这件事本质上就是让所有环节的“约定”对齐位宽对齐、运行时库对齐、存储顺序对齐三条都对齐剩下的就是敲代码了。
返回列表