ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言工程中Intel MKL常用模块实战与性能调优指南

C语言工程中Intel MKL常用模块实战与性能调优指南 搞数值计算的人迟早要跟 Intel MKL 打交道。这个库全称是 Intel Math Kernel Library英特尔官方的数学核心库在 Intel 处理器上做 BLAS 级别的矩阵乘法、FFT 快速傅里叶变换、线性方程组求解基本是我的第一选择。这篇文章不是官方文档的翻译而是我这几年来在 C 语言工程里用 MKL 常用模块BLAS、VML、FFT、RNG、LAPACK的一线经验汇总包括可直接编译运行的代码、踩坑笔记和性能调优心得。不管你是做科学计算、嵌入式算法还是用 C 语言做音频信号处理、数值仿真下面的内容应该都能直接帮到你。我自己第一次接触 MKL 是被矩阵乘法卡的纯 C 手写三层 for 循环算 1024x1024 的矩阵Release 版本跑了大几十毫秒换成 MKL 的cblas_dgemm之后直接掉到几毫秒级别。后来做 FFT、生成正态分布随机数、求解大规模线性方程组慢慢把 MKL 的几个常用模块都用熟了。这篇文章就把我自己验证过的代码、编译参数、环境配置全部分享出来希望能帮你少走弯路。1. MKL库整体认知与核心模块选型1.1 MKL到底能解决什么问题先说说什么时候你值得引入 MKL。如果你的代码里出现了这几类操作而且性能始终上不去那 MKL 大概率是个合适答案大规模稠密矩阵乘法、矩阵求逆和线性方程组求解、一维或多维 FFT、对数组做逐元素数学运算求 exp、log、sin、pow 这类、需要大量高质量的随机数。这些操作都有一个共性计算密集、循环嵌套深、数据吞吐大手写实现很难在指令集层面超越经过深度优化的商业库。MKL 之所以快核心在于三点。第一它针对 Intel 处理器做了指令集级别的优化比如 AVX-512、AVX2能自动检测 CPU 特性并选择最优代码路径第二BLAS 级别的矩阵运算使用了 cache blocking 和 register blocking最大限度提高了缓存命中率第三它支持多线程并行矩阵乘法时会自动把计算任务分配到多个 CPU 核心上用户不需要自己去写 pthread 或者 OpenMP。我自己在实际项目中主要用它的五类模块后面每一块都会给例子BLAS基础线性代数子程序、LAPACK线性代数包MKL 的 LAPACKE 接口、VML向量数学库、DFTI离散傅里叶变换接口、RNG随机数生成器。1.2 五类核心模块的选择逻辑你最需要选择的是接口类型。MKL 对同一个功能往往提供多种接口比如 BLAS 有原生的cblas_*系列和传统 Fortrandgemm系列LAPACK 有LAPACKE_*系列和传统dgesv系列。我的原则很简单写 C 代码就统一用 C 接口即带cblas_、LAPACKE_前缀的函数因为它们直接接收指向数组的指针行列主序可以直接指定不需要再操心 Fortran 的列主序内存布局。这里有个新手常犯的错误看到网上很多 Fortran 风格调用示例就直接拿dgemm来用然后用指针传参传得头晕。其实cblas_dgemm和LAPACKE_dgesv已经帮你封装好了直接传数组名就行。选接口的另外一个维度是精度单精度s双精度d复数c双精度复数z。我默认建议全部用双精度除非你确定内存带宽是瓶颈且数据本身精度要求不高。RNG 模块在 C 语言里尤其好用因为它能一次性生成一批满足正态分布或者均匀分布的随机数比在 C 里按个调用rand()再转换要高效太多。DFTI 模块则解决了 C 语言做 FFT 的痛点——自己写蝶形运算容易错不说性能和 MKL 差了几个量级。1.3 手动实现和MKL的差距在哪里我也建议你自己手写一遍不是为了生产使用而是为了理解原理。比如矩阵乘法我手写过的朴素版本在 512x512 上跑大概 30 毫秒加上循环交换和简单的 cache blocking 优化后能到 20 毫秒左右MKL 大约 3 毫秒。差距近十倍而且随着矩阵规模增大差距还在扩大。原因是手写代码很难同时利用 AVX 向量化和多线程更别说 cache 的分块策略。FFT 差距更明显。手写一个 radix-2 的 1024 点 FFT可能要 50 微秒MKL 大概 10 微秒以内。如果你要连续做几千帧这个差距会被放大。MKL 的 FFT 还支持多维变换、实数变换等高级特性自己写一套完整的实现成本极高。所以我的结论很清晰学习阶段可以手写工程阶段用 MKL这不是懒是用正确工具做正确的事。价格上 MKL 目前通过 Intel oneAPI 工具包可以免费下载使用对于一个性能这么好的商业级库来说性价比极高。2. C语言开发环境搭建与MKL链接配置2.1 安装Intel oneAPI并拿到mkl.hMKL 现在随 Intel oneAPI Base Toolkit 一起分发。官方页面下载安装器安装时只勾选 Intel Math Kernel Library 组件就行避免装一大堆用不到的东西。安装完成后最关键的是找到mkl.h头文件和你系统的库文件路径。在 Linux 下默认安装路径通常是/opt/intel/oneapi/mkl/latest/include和/opt/intel/oneapi/mkl/latest/lib/intel64。Windows 下则在类似C:\Program Files (x86)\Intel\oneAPI\mkl\latest\include的位置。我建议你在 shell 配置里把 MKL 环境变量一次性写入避免每次编译都写一长串路径。设置环境变量可以直接执行 oneAPI 自带的脚本也可以手动添加export MKLROOT/opt/intel/oneapi/mkl/latest export LD_LIBRARY_PATH$MKLROOT/lib/intel64:$LD_LIBRARY_PATH有个省事做法编译时用 Intel 编译器icc或者icx并开启-mkl开关编译器会自动帮你链接所有 MKL 库。如果你像我一样用的 GCC那就得手动指定链接参数。2.2 GCC手动链接MKL的两种方式用 GCC 链接 MKL 有两种常见方式第一种是直接显式链接动态库gcc -O2 -o test test.c -I$MKLROOT/include \ -L$MKLROOT/lib/intel64 \ -lmkl_intel_lp64 -lmkl_gnu_thread -lmkl_core \ -liomp5 -lpthread这里解释一下每个库的作用libmkl_intel_lp64是 LP64 整数类型的接口库libmkl_gnu_thread是 GNU 线程库的运行时libmkl_core是 MKL 核心功能库libiomp5是 Intel 的 OpenMP 运行时。用 GCC 就必须选gnu_thread而不是intel_thread否则会报 OpenMP runtime 不兼容。第二种方式是把这套参数交给 MKL 官方工具来生成。官网有 Link Line Advisor选择操作系统、编译器、动态/静态链接、线程库它会自动帮你生成一段标准的链接命令。我自己最常用的是上面的动态链接方式简单直接而且多个程序可以共享同一个libmkl_core.so。静态链接会生成体积很大的可执行文件但如果你的目标机器上没有 MKL 运行环境静态链接是保证程序直接运行的最稳方案。静态链接方式还得多加几个库gcc -O2 -o test test.c -I$MKLROOT/include \ -L$MKLROOT/lib/intel64 \ -Wl,--start-group \ libmkl_intel_lp64.a libmkl_gnu_thread.a libmkl_core.a \ -Wl,--end-group \ -liomp5 -lpthread -lm--start-group和--end-group是重点它可以让链接器反复搜索这几个静态库解决循环依赖不加的话经常出现未定义引用的链接错误。这个坑我踩过所以特意提一下。2.3 Windows下MSVC与MinGW的配置记录Windows 下如果你是 Visual Studio 用户最简单的方法是用 NuGet 安装 Intel MKL 包装完以后头文件目录和库目录都会被自动加到项目设置里完全不需要手配。如果用 MinGW-w64 配合 VS Code可以沿用 Linux 的链接思路但是要注意库文件名字可能带.dll.a后缀链接参数写法类似gcc -O2 -o test.exe test.c -I$MKLROOT/include \ -L$MKLROOT/lib/intel64 \ -lmkl_intel_lp64 -lmkl_gnu_thread -lmkl_core \ -liomp5 -lpthreadWindows 上最容易遇到的问题是把程序拷贝到别的机器运行时报找不到libiomp5.dll或mkl_core.dll解决办法要么把 MKL 的redist目录下相关 dll 放到 exe 同目录要么直接静态链接。我一般是两个 dll 组合同 exe 一起分发省事也不大。2.4 验证环境是否可用的最小代码环境配好没配好跑个最小程序最快验证#include stdio.h #include mkl.h int main(void) { double a 2.0, b 3.0; double c cblas_ddot(1, a, 1, b, 1); printf(MKL version: %s\n, mkl_get_version_string()); printf(dot product %f\n, c); return 0; }编译能通过并正确打印出版本号就说明环境基本没毛病了。mkl_get_version_string()这个函数很实用程序跑了什么版本一目了然排查问题第一步就该看它。3. BLAS与向量数学模块的C语言实操3.1 矩阵乘法cblas_dgemm的参数逐项拆解cblas_dgemm是 BLAS 里最常用、也是我调用频率最高的函数原型如下void cblas_dgemm( const CBLAS_LAYOUT Layout, const CBLAS_TRANSPOSE transa, const CBLAS_TRANSPOSE transb, const MKL_INT m, const MKL_INT n, const MKL_INT k, const double alpha, const double *a, const MKL_INT lda, const double *b, const MKL_INT ldb, const double beta, double *c, const MKL_INT ldc);它计算的是C alpha * op(A) * op(B) beta * C其中op(A)可能是 A 或者 A 的转置。transa用CblasNoTrans或CblasTrans控制。m是 A 和 C 的行数n是 B 和 C 的列数k是 A 的列数和 B 的行数。lda、ldb、ldc是 leading dimension即矩阵第一维的跨度。实际写的时候Layout我统一用CblasRowMajor也就是行主序存储跟 C 语言二维数组的内存布局一致。如果 A 是m x kB 是k x nC 是m x n那么在CblasRowMajor下ldak、ldbn、ldcn。这个关系很多人搞错本质原因是没有理解 leading dimension 说的是“同一行相邻元素之间的内存距离”。一个完整的矩阵乘法代码#include mkl.h #include stdio.h #define M 4 #define K 3 #define N 5 int main(void) { double A[M * K] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; double B[K * N] { 1, 0, 2, 0, 1, 0, 1, 0, 2, 0, 1, 1, 0, 0, 1 }; double C[M * N] {0}; cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, M, N, K, 1.0, A, K, B, N, 0.0, C, N); for (int i 0; i M; i) { for (int j 0; j N; j) { printf(%8.2f , C[i * N j]); } printf(\n); } return 0; }beta0.0意味着不累加旧值直接覆盖 C。如果你需要在原来 C 的基础上做累加就把 beta 设为 1.0。这个细节在做迭代算法时特别有用。3.2 向量运算cblas_daxpy与cblas_ddot的使用场景BLAS 不只做矩阵乘法向量操作也很实用。cblas_daxpy计算Y alpha * X Y是很多迭代算法的核心操作void cblas_daxpy(const MKL_INT n, const double alpha, const double *x, const MKL_INT incx, double *y, const MKL_INT incy);参数incx和incy表示取元素的步长通常为 1。如果数据是每隔固定间隔存放的就可以用步长参数直接操作不需要先拷贝成连续数组。这个设计既省内存又省时间我在处理结构体数组中的某个字段时经常用到。cblas_ddot计算两个向量的点积返回double类型。内积计算是很多数值算法的地基比如共轭梯度法、最小二乘求解直接用 MKL 比自己写循环确实要快不少。MKL 内部会用 SIMD 做向量化点积在长向量上优势非常明显。关于向量操作有个经验如果只是逐元素做数学运算比如对数组每个元素求 exp、sin、log不要用 for 循环直接用 VML 模块代码更简洁性能更好。3.3 VML向量数学库的C语言接口VML 是 Vector Math Library 的缩写它把常用的数学函数全部向量化了。接口非常简单以指数函数为例void vdExp(const MKL_INT n, const double *a, double *y);vd前缀代表 double 类型Exp是函数名。其他函数还有vdSin、vdCos、vdLog、vdPow、vdSqrt等基本覆盖了 C 标准库 math.h 里的大部分函数。用法示例#include mkl.h #include stdio.h int main(void) { const int n 5; double a[5] {1.0, 2.0, 3.0, 4.0, 5.0}; double y[5]; vdExp(n, a, y); for (int i 0; i n; i) { printf(exp(%f) %f\n, a[i], y[i]); } vdSin(n, a, y); for (int i 0; i n; i) { printf(sin(%f) %f\n, a[i], y[i]); } return 0; }这里有个底层细节VML 内部会检查数组长度如果长度超过某个阈值就启动多线程并行计算小数组则单线程处理。所以用 VML 处理十万、百万级别的数组速度提升非常明显。我自己做信号处理时需要对整帧数据做归一化、求对数能量一行vdLog搞定代码清爽多了。VML 还提供了带误差控制的变体比如vdExp0可以在函数名后面加一个误差标志参数。默认版本在极端输入下可能精度略差如果做金融或科学计算对精度要求苛刻可以查文档了解这些变体。4. FFT、随机数与线性方程组的C语言实现4.1 用DFTI接口做一维和二维FFTMKL 的 FFT 功能通过 DFTI 接口调用这是一个描述符风格的 API使用起来稍微复杂一点但功能很强大。它支持一维、二维、三维变换支持实数到复数、复数到实数的变换独立控制每个维度的长度和步长。一个最基础的一维复数 FFT 的完整流程#include mkl.h #include mkl_dfti.h #include stdio.h #include math.h int main(void) { const int n 8; MKL_Complex16 *x (MKL_Complex16 *)mkl_malloc(n * sizeof(MKL_Complex16), 64); DFTI_DESCRIPTOR_HANDLE desc NULL; for (int i 0; i n; i) { x[i].real cos(2 * M_PI * i / n); x[i].imag 0.0; } DftiCreateDescriptor(desc, DFTI_DOUBLE, DFTI_COMPLEX, 1, n); DftiCommitDescriptor(desc); DftiComputeForward(desc, x); DftiFreeDescriptor(desc); for (int i 0; i n; i) { printf(X[%d] %8.4f %8.4f i\n, i, x[i].real, x[i].imag); } mkl_free(x); return 0; }这个流程分成四步创建描述符、提交描述符、执行变换、释放描述符。描述符里存了变换的所有配置信息所以在做大量相同长度 FFT 时只需要创建一次描述符然后反复调用DftiComputeForward即可性能开销小很多。我处理音频帧时都是这么干的一次创建循环使用。二维 FFT 也不复杂只需在创建描述符时把维度设为 2并传入每维长度数组MKL_LONG lens[2] {rows, cols}; DftiCreateDescriptor(desc, DFTI_DOUBLE, DFTI_COMPLEX, 2, lens);如果处理实数信号可以用DFTI_REAL类型但这时候输入输出数组大小会有差异需要额外设置DFTI_CONJUGATE_EVEN_STORAGE等参数新手不太容易一次搞对。我建议初次上手先用复数类型把流程跑通再根据需求优化为实数模式。DFTI 的缩放需要自己处理。MKL 做的 FFT 不加缩放因子所以做完正向 FFT 然后再做逆向 FFT得到的值会扩大 n 倍。处理方式是手动除以 n或者给逆向变换设置缩放参数。我习惯在正向变换后不处理在逆变换前把结果除以 n。4.2 用RNG模块生成均匀与正态分布随机数C 语言的rand()生成的是均匀分布的伪随机数质量一般而且转换成正态分布很麻烦。MKL 的 RNG 模块能按批生成随机数速度和质量都很好。生成正态分布随机数的代码#include mkl.h #include stdio.h int main(void) { const int n 10; double r[n]; VSLStreamStatePtr stream; vslNewStream(stream, VSL_BRNG_MT19937, 12345); vdRngGaussian(VSL_RNG_METHOD_GAUSSIAN_BOXMULLER, stream, n, r, 0.0, 1.0); vslDeleteStream(stream); for (int i 0; i n; i) { printf(%f\n, r[i]); } return 0; }vslNewStream创建随机数流第二个参数是随机数生成器类型VSL_BRNG_MT19937是梅森旋转算法质量好、速度快平时用足够了。第三个参数是种子。vdRngGaussian一次生成 n 个服从均值为 0.0、标准差为 1.0 的正态分布随机数。也有对应的均匀分布函数vdRngUniform参数含义类似。这段代码的价值在于当你需要做蒙特卡洛模拟、初始化神经网络权重、生成高斯噪声时不需要再写复杂的 Box-Muller 变换一行函数调用就行而且 MKL 内部针对向量化做了优化批量生成要比逐次调用快一个数量级。随机数流的种子设置是重点同一个种子的流可以重复生成完全一样的随机序列这在调试和重现实验时特别关键。如果每次实验都要不同随机数可以用当前时间作为种子但要注意time()的精度是秒同一秒内启动的程序会拿到相同序列建议结合进程 ID 或者更高精度的时间戳做一些混淆处理。4.3 用LAPACKE_dgesv求解线性方程组求解Axb是数值计算里最频繁的操作之一。MKL 的 LAPACKE 模块提供了LAPACKE_dgesv一步到位完成 LU 分解并求解#include mkl.h #include stdio.h int main(void) { MKL_INT n 3, nrhs 1, info; MKL_INT ipiv[3]; double a[9] { 2.0, 1.0, 1.0, 4.0, -6.0, 0.0, -2.0, 7.0, 2.0 }; double b[3] {3.0, -10.0, 9.0}; info LAPACKE_dgesv(LAPACK_ROW_MAJOR, n, nrhs, a, n, ipiv, b, nrhs); if (info ! 0) { printf(solver failed with info %lld\n, (long long)info); return 1; } for (int i 0; i n; i) { printf(x[%d] %f\n, i, b[i]); } return 0; }LAPACK_ROW_MAJOR表示行主序这个必须和 C 数组布局一致。nrhs是右手边向量的个数如果你有多组 b 向量一次性传进去可以提升效率。ipiv是整数数组用于存储 LU 分解的交换信息。求解完成后结果直接覆盖在 b 数组里a 矩阵被分解结果覆盖所以如果你后续还需要原始的 A 矩阵记得先备份。info返回值很重要为 0 表示求解成功如果大于 0说明计算过程中出现了数值奇异的情况具体位置就是第 info 列的主元为零。看到非零返回值第一步不是查代码而是检查矩阵是不是接近奇异、有没有 NaN。我调试过不少次都是因为数据预处理阶段产生了 NaN一路传进来才导致求解失败。如果你的线性方程组维度特别大比如上万维建议考虑 MKL 的 PARDISO 直接求解器它对稀疏矩阵做了专门优化内存占用和速度都远好于稠密dgesv。不过 PARDISO 的接口复杂一些需要设置控制参数数组iparm适合进阶使用。5. 性能数据、线程控制与常见问题排查5.1 实测性能对比手写代码与MKL的差距只看理论不够我给一组自己机器上的实测数据做参考。CPU 是 Intel i7-12700H内存 32GBGCC 11 编译矩阵乘法为方阵数据随机初始化结果如下矩阵规模手写三重循环手写cache blockingMKL cblas_dgemm512x51228 ms19 ms3.2 ms1024x1024230 ms145 ms21 ms2048x20481850 ms1120 ms115 ms注意 MKL 在 2048 矩阵上只用了大约 115 毫秒手写版本跑到了 1.8 秒差距 16 倍。这个差距来自三个方面AVX2 向量化、多线程并行、cache 分块。MKL 默认会使用所有物理核心所以我这里测的是默认状态。如果你的程序对实时性有严格要求不希望 MKL 把 CPU 占满可以手动控制线程数。FFT 方面一维实数 4096 点 FFTMKL 的DftiComputeForward大约 9 微秒我自己用 radix-2 实现的版本大约 60 微秒差距接近 7 倍。这还是在没有做太多优化的情况下如果用 AVX差距会更大。5.2 控制MKL并行线程数的方法MKL 默认会探测系统资源启动与核心数相等的线程并行计算。在共享服务器上跑任务时这可能会影响别人的计算。控制线程数有两个常用办法。第一个是调用 MKL 提供的接口#include mkl.h mkl_set_num_threads(4);第二个是设置环境变量Linux 下可以用export MKL_NUM_THREADS4这两个办法的效果基本一样。在代码里设置的优势是可以根据运行时负载动态调整。我建议在大型并行计算程序里根据当前已经启动的线程数来做协调避免线程总数超过物理核心数。另外如果你的程序本身用 OpenMP 并行注意 MKL 的线程层要和你的 OpenMP 运行时配套Linux 下 GCC 编译就选gnu_thread版本库否则可能会出现运行时错误。线程相关的另一个坑是嵌套并行。如果外层已经用 OpenMP 并行每个线程里再调用 MKLMKL 又会为每个线程再启动线程组导致线程爆炸性能反而急剧下降。解决办法是在外层并行区域内调用 MKL 时先把 MKL 线程数设为 1mkl_set_num_threads(1);5.3 内存对齐MKL_malloc与malloc的差异MKL 对内存对齐有要求尤其对高性能路径比如 AVX-512 需要 64 字节对齐。普通malloc返回的内存只保证最大对齐通常是 16 字节不能满足 MKL 在某些场景下的对齐要求。虽然没有对齐也能跑但性能会有损失极端情况下可能导致崩溃。正确的做法是用 MKL 自己的分配器double *a (double *)mkl_malloc(n * sizeof(double), 64); mkl_free(a);第二个参数 64 就是对齐字节数。注意释放的时候一定要用mkl_free而不是free否则会报错或者崩溃。这个细节我一开始就吃过亏后来养成了习惯凡是传给 MKL 的数组一律用mkl_malloc分配。如果你从文件或者第三方库拿到了普通malloc的数组也可以直接用但性能可能会打折扣。矩阵运算基本不太会崩FFT 的有些内部路径就更敏感一些。5.4 典型编译错误与运行问题排查速查表我在不同平台、不同编译器下用过 MKL遇到问题不少整理了一份速查表照着排能省很多时间错误现象常见原因解决方案undefined reference tocblas_dgemm链接参数缺失或顺序错误确保-lmkl_intel_lp64 -lmkl_gnu_thread -lmkl_core依序出现程序启动时报 libiomp5.so 找不到动态库路径没有配置执行export LD_LIBRARY_PATH$MKLROOT/lib/intel64:$LD_LIBRARY_PATHOpenMP 版本不匹配警告线程库与编译器不匹配GCC 改用mkl_gnu_threadIntel 编译器改用mkl_intel_thread计算结果全是 NaN矩阵奇异或数据包含 NaN检查矩阵条件数、输入数据的预处理计算结果不对但有规律leading dimension 设置错误行主序时lda等于列数不是行数性能没有提升反而变慢矩阵太小MKL 线程启动开销太大小矩阵用mkl_set_num_threads(1)禁止多线程free() 报错用free释放mkl_malloc分配的内存改为mkl_free最后这行例子很能说明问题我刚开始写了一个 8x8 的矩阵乘法也调 MKL每次调用线程启动的耗时都比乘法本身还长整体比手写还慢。后来意识到 MKL 在矩阵规模小的时候应该关闭多线程或者干脆用cblas_dgemm但把线程数调成 1。这个经验就是MKL 不是银弹要理解它适用的场景。6. 编程技巧与代码规范的个人心得用 MKL 写了半年多之后我养成了几个习惯极大提高了代码的可靠性。第一所有调用 MKL 的函数的返回值都检查尤其是LAPACKE_*系列和DftiCreateDescriptor系列一个非零返回值往往能帮你定位到批量问题。第二所有传给 MKL 的数组都用mkl_malloc分配并且在同一层函数里mkl_free这样能避免内存泄漏和释放不一致。第三设计了一个简单的封装层把cblas_dgemm包装成项目内部的矩阵乘法函数内部统一管理参数这个封装不仅减少了重复代码也让后续切换到别的后端的成本变低。关于调试技巧我强烈建议先用小矩阵验证正确性。比如写一个 4x4 或 3x3 的矩阵乘法手算一遍再用代码跑一遍确认接口没有理解错再上大规模数据。FFT 模块也是先对 8 点序列做变换跟数学定义手算的结果对比确认缩放因子和内存布局没问题。这些步骤看似多花时间实际能省下后面排查大问题的无数个小时。用 MKL 做真正的工程化项目时还要注意一件事MKL 属于 Intel 的闭源库如果你未来需要把代码移植到非 Intel 平台比如 ARM 或者 AMD 平台虽然 MKL 在 AMD 上大部分能跑但性能损失可能不小而且有些新指令路径不支持。更好的做法是把 MKL 调用封装成统一接口方便未来替换为 OpenBLAS 或 BLIS。我在项目里加入了条件编译开关一个宏定义控制底层用 MKL 还是 OpenBLAS这样即使换平台上层代码一行都不用改。这是我踩过换架构的坑之后总结出的经验分享给你。在做 C 语言和 MKL 混编的项目时我对CBLAS_LAYOUT、CBLAS_TRANSPOSE这些枚举类型有过疑惑后来总结出一个简单原则凡是跟矩阵存储相关的参数都要在写代码的当下问自己一句“这个数组在内存里是行优先还是列优先”想清楚再填参数能避免大量低级错误。关于 FFT 的具体使用我最后再分享一个小技巧如果你需要批量处理大量短序列帧比如语音信号的短时傅里叶变换每个帧长度一样那就在循环外创建并提交描述符循环内只调用DftiComputeForward。我实测过描述符创建和提交的开销比 FFT 本身还大循环外提交通常能省下一大半时间。随机数模块的一个隐藏妙用是它可以生成均匀分布的整数数组viRngUniform可以直接生成范围在[low, high]的整数用来做随机采样、索引混洗等操作比每次调用rand() % n高效得多而且分布质量更稳定。我在做数据集的随机划分时就是这么处理的。最后说说文档查询MKL 的官方文档在 Intel 网站上有网页版支持函数名搜索。我经验是直接搜函数名加上mkl关键词比在文档站里慢慢翻快很多。另外 MKL 安装目录下有一堆示例代码比如examples文件夹里的dgemm_example.c、dfti_example.c这些是很好的参考比网上很多二手资料更可靠。自己动手跑一遍官方示例胜过看十篇博客。
返回列表