☰
DeepGEMM:通用矩阵乘法的性能优化与内核实现指南
2026/10/11 9:45:15 网站建设 项目流程

很多人第一次看到DeepGEMM这个项目名,第一反应是“又一个大模型框架”,第二反应才是“GEMM 是什么”。这里先给一个最直接的回答:GEMM 是 General Matrix Multiplication(通用矩阵乘法)的缩写,也就是 ( C = A \times B + C ) 这类基础运算。深度学习里它有多重要?全连接层、卷积层的 im2col 展开、注意力机制里的 QKV 投影,本质上都是一次 GEMM。DeepGEMM 这一类项目的目标,就是把矩阵乘法压到硬件极限,在推理和训练时把延迟和带宽成本砍下来。

这篇内容适合三类人:刚接触高性能计算、想搞懂算子为什么能快几十倍的开发者;已经在做推理优化、准备手写或魔改 GEMM 内核的工程向同学;还有仅仅好奇“为什么同一个矩阵乘法,有的库跑得快、有的库跑得慢”的算法从业者。我会从 GEMM 的瓶颈拆解开始,讲到数据布局、切分策略、混合精度这些核心设计,再给一套可以直接上手的内核实现思路和踩坑实录。整体偏实战,会有不少伪代码和参数层面的讲解,不涉及特定硬件平台的私有指令细节。

1. DeepGEMM 到底在优化什么:GEMM 的瓶颈拆解

1.1 为什么深度学习里 GEMM 是绝对主战场

先看一组很粗的账:一个大语言模型的参数通常在几十亿到上百亿,算一次 forward pass 要做多少次矩阵乘法?以某个模拟项目 X 的 7B 模型为例,单 token 推理时只算显式矩阵乘,大概就有几十次 shape 在 ((batch, seq) \times (hidden, hidden)) 量级的主计算。训练场景下,前向、反向、梯度更新各来一轮,矩阵乘法占总计算量的比例能轻松超过 80%。可以说,只要 GEMM 快 20%,整个模型的吞吐就能肉眼可见地涨一截。

这也是为什么几乎所有推理框架、训练框架里,GEMM 相关算子都被单独拎出来做极致优化,而不是像普通算子那样直接交给编译器默认生成。DeepGEMM 这种项目做的事情,本质上就是那块最硬的骨头:在给定的硬件上,让矩阵乘法跑出接近理论峰值的实际性能。

1.2 朴素矩阵乘法和极致实现差在哪里

用最朴素的三重循环写一个矩阵乘法:

for (int i = 0; i < M; ++i) for (int j = 0; j < N; ++j) for (int k = 0; k < K; ++k) C[i][j] += A[i][k] * B[k][j];

逻辑上完全正确,但性能往往只有理论峰值的 5% 到 10%。问题出在哪里?两件事:第一,内存访问不连续,内层循环里访问 B[k][j] 是在按列跳着读,每次读一个浮点数就要等一次内存事务;第二,完全没有复用,A[i][k] 被 C[i][j] 的每一列都读一遍,B[k][j] 也被每一行读一遍,数据在 CPU 缓存和内存之间来回搬运,计算单元大部分时间都在等待数据。

DeepGEMM 这类项目做的事,就是围绕这两个问题做文章:让访存变成连续的、块状的行为,让数据在寄存器、L1、L2 缓存里尽可能重复利用,最后才能把计算单元喂饱。理解了这个起点,再看后面所有优化手段,会发现它们全是在解决“数据喂不到计算单元嘴里”这个核心矛盾。

1.3 计算峰值与访存带宽的账

评估一个 GEMM 实现是否还有优化空间,建议先做一次“纸面估算”。假设目标硬件上,单精度浮点算力是 X TFLOPS,当前矩阵乘法实际跑出来的时间是 T,那么实际 FLOPs 就是 ( 2 \times M \times N \times K / T )。拿实际值除以理论峰值,就是所谓的计算效率(Compute Efficiency)。

但还有另一条线:访存效率。一个 ( M = N = K = 4096 ) 的 FP32 GEMM,输入输出数据总量大约是:

[ 4096 \times 4096 \times 4 \times 3 = 201 \text{ MB} ]

假如内存带宽是 200 GB/s,光搬数据就要 1 秒,而硬件算力如果支持在 0.1 秒内完成计算,那就是典型的访存瓶颈。反过来,如果数据能全部留在片上缓存里,重复读取的次数就能从“每个输出都重新读一遍”降到“每个数据只读一次”,这个差别在规模一大之后就是数量级的。所以每次拿到新的 GEMM 任务,我第一件事永远是算这比账:当前实现是计算受限还是访存受限,再决定下一步优化方向。

2. 设计思路与关键技术选型

2.1 数据布局:为什么行主序会“绊脚”

深度学习框架里,矩阵默认的行主序(Row-Major)布局看起来是合理的:A[i][k] 和 B[k][j] 用同一个 i、k 索引访问,逻辑上很好写。但到了高性能实现里,这个布局会让 B 矩阵的内层循环直接变成跨行跳跃访问,带宽利用率非常难看。

所以 DeepGEMM 这类项目普遍会在 GEMM 内核里做一次布局转换:B 矩阵从 ( K \times N ) 的行主序,变成 ( N \times K ) 的列主序存储,或者说等效于把 B 做了转置。这样内层循环访问 B 的一整行时,地址是连续的。有人会问:转置本身不也要花时间吗?是的,但转置可以放在初始化阶段或者算子融合阶段,分摊到大量计算里,代价几乎可以忽略。相比之下,每次迭代都跳着访问内存的损失是要命级别的。

实测下来,在一个模拟的 FP32 GEMM 场景里,仅把 B 转置成连续布局,性能就能提升 20% 到 40%,具体看矩阵规模。如果规模太小,比如 ( 16 \times 16 ),转置的启动开销会吃掉收益,所以还得配合后面要讲的 Tile 切分来判断到底值不值。

2.2 Tile 切分:用尺寸换 Cache 命中和寄存器复用

朴素三重循环的直接问题是,内层循环的 k 维度跨度太远,Scratchpad 和缓存根本接不住。标准解法是分块(Tiling):把矩阵切成一个个小的子块,每个子块在计算时完全放进 L1/L2 缓存,块内部反复读取时就不再触碰内存。

先看一个简化但有效的分块策略。设分块尺寸为 ( BM \times BN \times BK ),对应 A 的子块是 ( BM \times BK ),B 的子块是 ( BK \times BN )。计算时外层循环遍历所有子块,内层循环在一个子块内部做完整的 GEMM。常见的选择是 ( 64 \times 64 \times 16 ) 或者 ( 128 \times 128 \times 32 ) 这类尺寸。

为什么选这些尺寸?有一个经验公式:单个线程/计算核的寄存器文件大小和 L1 缓存大小是硬限制。比如某目标平台有 256 KB L1,那么 ( BM \times BK \times 4 + BK \times BN \times 4 + BM \times BN \times 4 ) 的总字节数必须远小于 L1。以 ( 64 \times 16 ) 的 A 子块和 ( 16 \times 64 ) 的 B 子块算,总共是 ( 64 \times 16 \times 4 + 16 \times 64 \times 4 + 64 \times 64 \times 4 = 20480 ) 字节,也就是 20 KB,放进 L1 完全没问题。此时内部循环可以重复读这些数据很多次,内存带宽的压力就大大缓解了。

分块规模也不是越大越好。块太大,缓存放不下,会开始发生缓存驱逐,反而退化成内存访问;块太小,复用的机会减少,性能上不去。所以核心思路是:让块的大小贴着一级缓存容量走,同时确保每个块内的计算量足够大。

2.3 混合精度与数值策略:FP32 累加器的关键位置

DeepGEMM 项目里另一个躲不开的话题是精度。现在 AI 推理普遍用 FP16 或 BF16 做输入输出,如果直接拿两个 FP16 做乘加,累加器还是 FP16,很快就会出现精度灾难。标准解法是:输入数据用 FP16/BF16 存储和读取,乘出来的中间结果在寄存器里累加时,使用 FP32 累加器。这样既节省了带宽和存储开销,又保留了足够的数值动态范围。

在具体实现里,这一步通常意味着内层循环是这样的伪代码:

float acc[BM][BN] = {0}; // FP32 累加器 for (int k0 = 0; k0 < BK; ++k0) { half a_val = A_block[i][k0]; half b_val = B_block[k0][j]; acc[i][j] += (float)a_val * (float)b_val; }

关键点是“什么时候转成 FP32”。很多人在最初实现时图省事,在读取 A、B 时就先转成 float,再到寄存器里做乘加。这样做的坏处是:内存读取带宽直接翻倍,因为读进来的数据从 2 字节变成了 4 字节。正确做法是:数据保持 FP16/BF16 读入,在寄存器内部才展开成 FP32 做计算,这样内存侧只承受 2 字节的带宽成本,计算侧又有 FP32 的精度兜底。这一步对性能的影响很大,在我实测过的一个模拟内核里,单这个细节就能拉开 15% 以上的差距。

2.4 为什么需要指令级手写,而不是依赖编译器

看到这里有人会问:这些优化听起来都是 C++ 层面的思路,编译器难道不能自动做吗?答案是:编译器能做一些循环展开和向量化,但很难替你完成“寄存器级的分块决策”。因为 GEMM 的最优分块参数依赖于目标硬件的寄存器数量、缓存层级、SIMD 宽度,这属于硬件特性的范畴,编译器很难在通用层面生成最优解。

所以 DeepGEMM 这类项目往往会在关键内层循环里使用 intrinsics——也就是直接调用硬件提供的向量指令,或者在汇编层面手写一小段核心循环。这在普通应用代码里是大忌,但在 GEMM 内核里是常规操作。因为内核代码只有几十行,且在所有计算中重复执行的次数极高,手写那几十行带来的收益远超维护成本。

我个人的建议是:第一步先用纯 C++ 把逻辑跑通、性能对标到“比朴素实现快 5 到 10 倍”的水平,再考虑是否用 intrinsics 做最后 30% 的压榨。如果一上来就手写汇编,调试难度会急剧上升,而且很容易在没吃透 Cache 行为的情况下做出负优化。

3. 实操过程:手写一个可用的 DeepGEMM 核心计算内核

3.1 从伪代码到可运行的 C++ 模板内核

这部分给出一个可以直接参考的结构。假设目标环境是 x86 平台,支持 AVX2,数据类型是 FP16 输入、FP32 累加,先写一个不依赖平台私有 intrinsics 的 C++ 版本,重点讲清楚分块和布局转换怎么组织。

第一步:重排 B 矩阵布局。原始 B 是 ( K \times N ) 的行主序,先把它转成 ( N \times K ) 的连续存储,这样内层循环可以顺序读 B 的一行。

// B_transposed[n][k] = B[k][n] std::vector<half> B_T(N * K); for (int k = 0; k < K; ++k) for (int n = 0; n < N; ++n) B_T[n * K + k] = B[k * N + n];

第二步:按 Tile 遍历。外层循环处理所有子块,内层把子块数据搬进局部缓冲区,然后再做局部 GEMM。先不用管复杂性,把逻辑写清楚,再慢慢优化。

constexpr int BM = 64, BN = 64, BK = 16; for (int i0 = 0; i0 < M; i0 += BM) { for (int j0 = 0; j0 < N; j0 += BN) { float acc[BM][BN] = {0.f}; for (int k0 = 0; k0 < K; k0 += BK) { // 把子块 A[i0 : i0+BM][k0 : k0+BK] // 子块 B_T[j0 : j0+BN][k0 : k0+BK] 载入局部内存 half a_block[BM][BK]; half b_block[BN][BK]; // ... load with boundary checks ... for (int i = 0; i < BM; ++i) for (int j = 0; j < BN; ++j) for (int k = 0; k < BK; ++k) acc[i][j] += (float)a_block[i][k] * (float)b_block[j][k]; } // 写回 C[i0 : i0+BM][j0 : j0+BN] } }

核心逻辑已经在这里了。接下来要做的所有优化,都围绕“怎么让内层那个三重循环跑得更快”:把 a_block 和 b_block 的读取改成连续,把 acc 数组尽量放到寄存器变量里,把循环展开到合适的粒度。

一个重要的实操点:不要一上来就写边界检查,因为每次内层循环里加 if 判断会有分支开销。正确做法是把矩阵填充到对齐的尺寸(比如 M 向上取整到 BM 的整数倍,填充部分用 0),这样内层循环就是完全无分支的。这也是我反复强调的“用内存换分支”思路。

3.2 极端 Case 处理与 Shape 泛化

实际业务里不会总是方方正正的矩阵,M、N、K 可能是任意整数。所以 DeepGEMM 的实现必须处理“边界”问题。处理方式有两种主流方案:

第一种,填充法,上面已经提到。把 M 向上对齐到 BM,N 向上对齐到 BN,K 向上对齐到 BK。新增的部分用 0 填充,不影响计算正确性。这个方案实现最简单,缺点是一次性分配多出一点内存。对于推理场景中的固定 shape,这是最优解,因为填充只在启动时做一次。

第二种,分支法,在每次加载子块时判断边界里的有效数据数量。这个方案对任意 shape 都成立,但内层循环里有条件跳转,性能会略有下降。实测下来,当 M、N 恰好是 Tile 整数倍时,分支法的性能可以做到和填充法几乎一致;但如果不是,分支法可能会慢 10% 到 20%。

我的建议是:框架内部统一用填充法,因为算子层的 shape 基本是编译器静态已知的。如果写的是动态 shape 的通用算子,就在 dispatch 层套一个分支:shape 对齐时走快速路径,不对齐时走通用路径,两条路径共用同一套内核模板。

此外还有 A 矩阵和 B 矩阵的转置标志问题。深度学习里经常出现“对 A 做转置”或者“对 B 做转置”的需求,比如 attention 里的 score 计算就常常是 ( Q \times K^T )。如果每种布局组合都写一个内核,代码量会爆炸。标准做法是:统一把输入布局在初始化阶段重排成内核需要的最优布局,内核本身只支持一种布局。这个思路看起来多了一步拷贝,但胜在 Debug 简单,而且可以规避大量分支。

3.3 指令级优化方向:SIMD 与寄存器分块

当 C++ 版本的逻辑稳定后,就可以开始考虑指令级优化。这一步的核心目标是把内层循环变成:每次从内存取一批 A 子块和 B 子块数据,在寄存器里做一批乘加,再写回。我用 AVX2 平台举例,思路在其他 SIMD 平台通用。

AVX2 的向量宽度是 256 bit,可以一次处理 8 个 FP32,或者 16 个 FP16。如果做 FP16 输入、FP32 累加,一种方案是把 16 个 FP16 拆成两组 8 个,转成 FP32 后分别用两条向量指令做乘加。伪代码大致是:

__m256 acc_vec[BN_VEC]; for (int k = 0; k < BK; k += 8) { __m128i a_half = _mm_loadu_si128(...); // 8个FP16 __m256 a_float = _mm256_cvtph_ps(a_half); // 转成8个FP32 // 对 B 做同样操作 for (int jj = 0; jj < BN_VEC; ++jj) { acc_vec[jj] = _mm256_fmadd_ps(a_float, b_float[jj], acc_vec[jj]); } }

这个方向优化的空间非常大,核心在寄存器分块参数的选取。寄存器数量是有限的,你要决定:一次循环里同时算多少行、多少列的输出。比如把 ( 8 \times 8 ) 的 C 子块放在寄存器里,那么需要 8 个向量寄存器放累加器,另外还要几个寄存器放 A 片段和 B 片段。如果贪心选 ( 16 \times 16 ),寄存器不够用,编译器就会把部分累加器溢写到栈上,性能反而暴跌。

我踩过的坑是:一开始为了“看起来更并行”选了很大的寄存器分块,结果寄存器溢出导致性能比编译器自动生成还差。调试了很久才发现,加一个-fverbose-asm看一下汇编,里面全是vmovups和栈操作,根本不是计算指令。后来老老实实从 ( 8 \times 8 ) 开始测试,再逐步加大,才找到了当前平台的最优参数。

4. 性能验证与常见问题排查

4.1 测试工具与性能基线对比方法

评估一个 GEMM 内核是否合格,不能只看“跑得快”,还要看相对基线的提升幅度。我建议准备三组数据做对比:

第一组是朴素三重循环实现,直接作为性能下限基准。第二组是当前框架里成熟的 BLAS 库实现,作为业界水准参照。第三组就是你自己写的 DeepGEMM 内核。每次修改只动一个小变量,保持其他条件不变,记录时间。

性能测试的步骤也很关键。矩阵乘法的耗时受数据冷热状态影响极大,第一次调用会把数据从主存搬到缓存,后续调用会命中缓存,时间明显变短。正确做法是:先跑若干次 warmup,让缓存状态稳定;然后跑 50 到 100 次计时取中位数,而不是取最小值或平均值。平均值会被偶发的系统调度干扰,最小值往往只是冷启动时碰巧没被中断打断,中位数最稳定。

FLOPs 的计算也要准确。一次标准的 ( M \times N \times K ) GEMM,浮点操作次数是 ( 2 \times M \times N \times K )(乘法和加法各一次)。用计时时间去除这个数,就得到实际算力。注意很多人在 K 维度上算错,把 ( 2 \times M \times N \times (K-1) ) 或漏掉加法操作,导致百分比虚高。

4.2 排查实录一:数值不一致,越优化越错

一个常见的诡异问题是:内核写对了,跑小矩阵时结果正确,但矩阵一大,结果就开始出现微小的偏差,有时甚至直接出现 NaN。

这类问题第一嫌疑是边界越界。当 M 不是 BM 的整数倍,而填充又没做好时,内层循环会读到未初始化内存,可能是任意值,包括极小的浮点数或规格化数,累积到 FP32 累加器里就会产生不可控的误差。排查方法不难:在 main 函数里把整个输入矩阵初始化为 0,并开启 AddressSanitizer 跑一遍,如果提示堆缓冲区溢出,基本就是这里。

第二嫌疑是 FP16 表达的动态范围不够。某些模型权重绝对值在 ( 1e-4 ) 到 ( 1e-4 ) 之间的稀疏值很多,转成 FP16 后直接变成 0,或者产生非规格化数,累加后和原始 FP32 结果差得多。这种情况不能怪内核,只能改用 BF16 或者在算子层面做误差补偿。

第三嫌疑是 cache line 伪共享,多见于多线程版本里多个线程写同一个缓存行。比如两个线程各自算 C 矩阵相邻的两列,如果列间距很小,它们会频繁争抢同一个 64 字节缓存行,速度急剧下降,数值也可能因为非原子操作而互相覆盖。解决思路是:每个线程负责的 C 子块至少按缓存行对齐,比如每个线程至少处理 8 个 FP32 列,避免共享。

4.3 排查实录二:性能倒挂,手写内核反而更慢

很多人第一次把 DeepGEMM 写完后去对标 BLAS 库,结果发现自己的版本慢得离谱,甚至比朴素实现还慢。这里我梳理几个高频原因:

第一个原因是内存分配的开销。每个子块都new一块局部内存,循环里反复分配释放,性能基本就废了。正确做法是在内核外部一次性分配好所有工作缓冲区,复用同一块内存。

第二个原因是编译器优化没有生效。C++ 模板内核看起来逻辑正确,但编译器可能没有把局部数组放到寄存器里,而是一股脑地放到了栈上。用-O3 -march=native编译是基本操作,另外可以试着把内层循环里访问的数组声明为register或使用局部变量让编译器更容易识别。更保险的做法是检查生成的汇编里内层循环有没有向量化指令,如果全是标量操作,就说明编译器根本没听你的。

第三个原因是你可能没有启用多线程。现代 CPU 的 GEMM 性能峰值是多核协同的结果,单核实现即便做得再极致,也顶多达到一部分性能。但多线程不是简单加个#pragma omp parallel for就完事的,线程数、任务划分粒度、线程亲和性都影响结果。我通常的实践是:先验证单核版本已经达到单核理论峰值的 70% 以上,再开始做多线程扩展,否则问题叠加起来很难排查。

第四个原因是矩阵规模太小。GEMM 内核的启动开销和 Tile 切分开销是固定的,当 M、N、K 只有几十的时候,这些开销会淹没计算收益。这类规模在业务里也很常见,比如某些很小的全连接层。针对小矩阵,另有专门的 kernel 设计策略,通常不会走BM=64这种大切分路线。

4.4 避坑清单:DeepGEMM 开发中的高频问题和应对

整理一份简表,按影响程度排列:

问题主要症状应对方案
B 矩阵布局未重排性能远低于预期统一转换为 ( N \times K ) 连续布局
内层循环有边界 if性能不稳定、分支预测开销高填充到对齐尺寸,消除分支
局部数组导致栈溢出编译后的汇编全是 vmovups使用寄存器变量或手动展开
多线程伪共享核数增加但性能不升反降按缓存行对齐任务划分
FP16 累加精度不足大矩阵结果偏差明显切换 BF16 或使用 FP32 累加器
冷缓存计时性能数据波动大warmup 后取中位数
寄存器分块过大汇编出现栈溢出操作缩小寄存器分块,逐步逼近最优

这份清单基本覆盖了我自己在做 DeepGEMM 类项目时遇到的绝大多数问题。有些问题光看代码很难发现,但只要对汇编和内存行为有一定敏感度,可以靠经验快速定位。

5. 从算子到系统的扩展方向

5.1 融合算子与 Dequant:GEMM 绝不单打独斗

GEMM 内核本身的优化做到一定程度后,边际收益会越来越小。这时候真正的系统级优化开始登场:把 GEMM 前后的算子融合进同一个内核,减少中间结果的写回和读入。典型例子是量化模型的 Dequant 融合:如果权重是 INT8 或 FP16,而计算需要反量化回高精度,传统做法是先跑一遍反量化算子,把结果写到内存,再跑 GEMM。融合方案则是在加载 B 子块数据时,直接在寄存器里做反量化,然后马上参与乘加。这个操作省掉的是一整轮 ( K \times N ) 矩阵的读写,收益极其可观。

融合后的实现需要注意寄存器压力。Dequant 操作通常需要额外的查找表或缩放因子,处理不好就会挤占累加器的寄存器空间。实践上可以多测几种融合策略,比如有的平台把缩放因子做成标量乘法,有的平台要做向量查表,选在当前硬件上表现最稳的那种。

5.2 多核并行与负载均衡

当单核内核稳定后,下一步就是让多个计算核同时干活。任务划分的核心原则是:每个核处理一块尽量独立的 C 子块,避免共享写入。常见做法是,把 C 矩阵按行方向切分成若干连续带,每个核负责一条带。如果矩阵高度 M 非常大,可以做两层切分:第一层按行切分出大任务块,第二层在块内再按列切成小片,便于动态负载均衡。

负载不均衡的问题在真实场景里很常见。比如 M=100、N=10000 这种形状,如果按行切分,某些行因为没有足够 K 维度计算,耗时很短,某些行很长,导致一部分核提前空闲。应对方案是使用任务队列而不是静态切分:每个核完成一个任务片后去队里取下一个,这样就算任务大小差异很大,总体也能保持较好的负载均衡。

我自己的经验是:做多核扩展时,先在单核版本上用perf统计出计算和访存的比例,确认瓶颈类型。是访存受限时,加再多核收益也有限,因为内存带宽已经被吃满了;是计算受限时,加核心数才有效果。很多项目组调了半天线程数没有提升,其实是没意识到早就是带宽瓶颈了。

5.3 值得继续深挖的几个延伸方向

DeepGEMM 这类项目可以延伸的方向其实不少,简单列几个我觉得后续价值比较高的:

稀疏 GEMM。大语言模型里越来越多的权重是稀疏的,把零值跳过不计算,能进一步压计算量和带宽。但稀疏矩阵的存储格式和并行策略跟稠密版本差异很大,建议单独开一个子项目去探索。

单算子多版本自动调优。不同 shape 下最优 Tile 参数完全不同,可以把常见 shape 对应的最优参数离线搜索一次,存成配置表,运行时按 shape 查表选择版本。

与编译器协同。手写内核虽然性能极强,但开发成本高。如果能把你验证过的最优 Tile 参数和布局策略沉淀成编译器 hint,让编译器在更广泛的算子上自动生成类似代码,对整个框架的性能都是正收益。

这些方向里,自动调优是性价比最高的。因为 GEMM 的优化空间是分层的,只要把某一层的最优参数固定下来,后续的新硬件支持就只需要重新跑一遍搜索流程,不用再重新设计内核。

回到开头那句话:GEMM 是深度学习的绝对主战场。DeepGEMM 这类项目教会我的最重要一课是,性能优化不是玄学,而是“理解硬件 + 拆解访存 + 反复实测”的工程循环。如果你正准备开始做自己的 GEMM 内核,我的建议是别一上来就追求极致,先把 C++ 版本跑通,把性能对齐到朴素实现的 5 倍以上,再来触碰指令级优化——等你走到那一步,会发现之前所有在布局和切分上花的时间,全部会折算成最终性能的一部分。

最后分享一个实用技巧:调试 GEMM 内核时,永远用一个很小的例子(比如 8×8×8)和一份朴素实现做对拍,每次改完代码都先跑这个小例子的结果对比,再跑大规模性能测试。这一步能防止你把性能优化和正确性调试搅在一起,省下的时间远比你想象的多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询