资讯动态

矩阵乘法优化实战:DeepGEMM从零打造高性能推理内核

发布时间:2026/10/26 3:11:59 来源:尧图企业网站定制
矩阵乘法也就是GEMMGeneral Matrix Multiplication可能是整个深度学习系统里被提及最多、又最少被普通工程师真正读懂的一个词。它出现在全连接层、卷积的im2col变换、注意力机制里的QKV投影、推荐系统的Embedding查找与交互几乎每一个上规模的模型推理任务最终都会落到一堆GEMM调用上。DeepGEMM这个项目就是我自己从零写的一个面向深度学习推理场景的高性能矩阵乘法库目标是搞明白一件事在尽量少的依赖下仅凭硬件特性、内存层级和指令级并行能把一个朴素的三重循环优化到什么样的速度。这篇文章会从GEMM为什么重要、为什么不用通用库、核心优化思路到分块和微内核的具体写法再到精度和性能问题的排查完整梳理一遍做DeepGEMM的思考过程。适合三类人看正在做推理引擎或者算子融合的工程师想入门高性能计算但不知道从哪里下手的同学以及虽然天天调库但想知道底层到底在干什么的人。看懂这篇文章不需要很深的背景但看完了你会对“为什么矩阵乘法占了大头”这件事有完全不一样的体感。1. 为什么需要自研GEMM——矩阵乘法在深度学习里的位置1.1 从全连接到注意力机制GEMM无处不在最直观的例子是全连接层。一个形状为[M, K]的输入矩阵乘以形状为[K, N]的权重矩阵得到[M, N]的输出。这里M通常是批量大小乘序列长度K是输入特征维度N是输出特征维度。Transformer里每个Attention头的Q、K、V投影是三个GEMM计算注意力分数Q K^T是一个GEMM注意力分数与V相乘又是一个GEMM。算下来一次标准推理里GEMM的耗时占比超过80%非常正常。卷积层算不算GEMM严格说卷积是四维运算但主流的实现方式比如im2col加GEMM或者隐式GEMM核心还是矩阵乘法。甚至现在很多推理引擎会把卷积直接转化成GEMM来处理因为矩阵乘法的优化思路足够成熟优化一次卷积、全连接、Attention都能受益。从计算量角度看GEMM的浮点运算次数是2 * M * N * K。哪怕一个很小的模型一次矩阵乘法的FLOPs也是百万级起步一个7B模型跑一遍前向要做的矩阵乘法运算量足够让CPU忙很久。所以GEMM性能直接决定了模型的响应延迟和吞吐量这不是“值得优化”的问题而是“不优化就没法用”的问题。1.2 通用优化库的局限既然GEMM这么重要那直接用官方数学库不就行了吗行大部分场景下也确实应该这么用。但自己做DeepGEMM的动机在于通用库有三个很难绕过去的局限。第一通用库是为“所有形状”优化的不是为“你的形状”优化的。通用库要覆盖各种M、N、K的组合各种对齐条件各种数据类型和布局需求策略必须非常保守。比如M很小的时候M1、M4走大分块的GEMM算法反而不如专门的小矩阵路径比如K特别大但N很小访存模式和均衡GEMM完全不同。模型推理时形状往往是固定的可以针对固定形状做极其激进的优化收益经常是两位数百分比的。第二量化推理正在成为主流但通用库对INT8、FP8、INT4这些类型的支持往往不够灵活。做量化GEMM不只是把数据类型换成整数那么简单还要处理scale、zero point的融合时机以及避免中间反量化造成的额外开销。通用库给了你接口但不会为你的量化方案定制优化。第三算子融合做不到。推理引擎里GEMM之后通常跟着BiasAdd、激活函数、残差连接如果能把这些融合进GEMM的尾端就不用把中间结果写回显存再读出来。这一来一回省下的带宽是非常惊人的。通用库的黑盒接口不会帮你做这件事你只能自己维护一个GEMM内核。除此之外学习价值也很高。GEMM是高计算密集度、高数据复用率的代表性问题把GEMM优化吃透了做卷积、做带宽优化、做图融合都会轻松很多。所以从工程和成长两个角度DeepGEMM都值得做。1.3 影响范围为什么一个算子能撬动整个推理链路GEMM不是孤立存在的它的性能会沿链路传导。举个我实际遇到的例子某个模拟项目X的模型一次推理里有几十次GEMM调用量化的GEMM只占其中几次但修改内核后整体延迟下降了接近一半。原因就在于矩阵乘法往往是拖尾算子只要它变快了后续算子才能空出资源。更关键的是GEMM内核可以被复用。同一个内核换不同的分块参数就能同时服务全连接层、Attention投影和卷积场景。这意味着你在GEMM上做的一点点优化会被整套推理框架放大无数倍。这是做底层算子最有成就感的地方。2. 核心设计思路从访存到计算的对抗游戏2.1 内存层级与算术强度要理解GEMM优化必须建立一张硬件内存的层级图。寄存器最快但容量最小通常一个核也就几十到几百个寄存器L1缓存次之每个核几十KBL2缓存大一些但延迟已经明显上升最后是主存容量大但带宽有限。数据在层级之间搬运的成本比计算的成本高出好几个数量级。这里有一个关键概念叫“算术强度”也就是“每访问一次数据能做多少次浮点运算”。GEMM的算术强度天然是高的——2*M*N*K次运算只需要读入M*K K*N个数据。理论上数据只要读进缓存一次就可以疯狂复用好多次。但前提是你的循环顺序必须让数据在缓存里待得足够久否则每次运算都从主存取数算术强度再高也白搭。打个比方。你在厨房做菜冰箱是主存台面是L2缓存切菜板是L1缓存手是寄存器。如果每切一片菜都跑去冰箱拿一次食材那大部分时间都浪费在路上。正确的做法是一次把所有食材搬到台面上再到切菜板上小批量处理。GEMM优化的核心就是安排好这套“搬菜”和“切菜”的节奏。2.2 分块Tiling三大核心参数M_BLOCK、N_BLOCK、K_BLOCK朴素GEMM是一层一层的三重循环遍历M、N、K逐个点算。由于内层循环会反复访问A和B的同一片数据但访问粒度太小缓存根本来不及“意识到”数据可以被复用。分块也就是Tiling就是解决这个问题的手段。我把它拆成两层分块。第一层把矩阵切成大块。对于输出的C矩阵按M_BLOCK x N_BLOCK切成一个一个小块输入A按M_BLOCK x K切B按K x N_BLOCK切。这一层的目标是把数据分成适合L2缓存的“托盘”。第二层在每一个M块和N块内部再把K维切成K_BLOCK的小段。这样一次只算“一块”数据让A和B的片段分别待在L1缓存里。两层分块配合起来数据复用率能提高一个数量级。三个分块参数怎么选关键看三件事缓存容量、寄存器数量、并行度。M_BLOCK和N_BLOCK越大片上数据复用率越高但每个线程块占用的寄存器和缓存资源就越多同时能并行的线程块数就越少。K_BLOCK太大L1缓存装不下会频繁驱逐太小则无法有效摊平访存延迟。我常用的一组起步值是M_BLOCK64、N_BLOCK64、K_BLOCK16这组参数在形状适中的矩阵上表现非常稳之后再针对具体形状调。注意分块不是越大越好。块太大导致一个核同时处理的线程数变少利用率反而下降。正确的姿势是固定整体占用率比如每个核的占用率控制在百分之几十再在这个前提下尽量加大分块。2.3 数据复用与向量化让每一条加载指令“物超所值”分块解决的是“数据重用”但还有一个维度是“单次加载的利用率”。现代处理器的向量寄存器可以一次装载多个浮点数比如支持128位SIMD的CPU可以一次加载4个FP32支持张量核心单元的GPU硬件则可以在单指令中完成大矩阵的乘加操作。为了让向量化收益最大化GEMM里有一个经典技巧把A矩阵的一行分给多个线程去同时复用让同一份数据被广播到多个处理单元。以CPU上的SIMD为例内层循环里向量寄存器一次性装载B矩阵的4个连续元素然后与A矩阵的1个标量相乘结果累加到4个独立的累加器上。这样一次内存访问完成了4次乘法累加带宽成本被摊薄了。这里还要提到“累加器数组”的概念这也是微内核的设计基础。C矩阵的每个碎片不是每算一个点就写回内存而是先放在寄存器数组里反复累加K个维度之后再一次性写回。比如一个4x4的C碎片需要16个寄存器来维护累加结果。寄存器是GEMM优化里最金贵的资源设计微内核时一定要确保累加器全部放在寄存器里不要让编译器把它们“溢出”到栈上。3. 从朴素实现到高性能实现——实操路径拆解3.1 第一步朴素的三重循环先建立正确性基线任何高性能实现的前提都是有一个完全正确的朴素版本。它不仅能用来验证后续优化版本的正确性还能作为性能对比的基准。void gemm_naive(const float* A, const float* B, float* C, int M, int N, int K, float alpha, float beta) { for (int m 0; m M; m) { for (int n 0; n N; n) { float sum 0.0f; for (int k 0; k K; k) { sum A[m * K k] * B[k * N n]; } C[m * N n] alpha * sum beta * C[m * N n]; } } }这个版本没什么好说的唯一要建立的习惯是从一开始就支持alpha和beta。很多GEMM调用场景都需要把结果累加到已有的C矩阵上比如残差连接、某些特殊融合场景。后期再把alpha、beta加进去会非常痛苦。内层循环里A的一行和B的一列被反复访问且每次只算一次乘法。当K比较大的时候每次读入的B列元素从上一次开始就被缓存淘汰掉了。所以这个版本的性能基本等于内存带宽上限没有任何复用。验证这个版本时我会用M、N、K都在32到256之间的几个随机形状用统一的随机种子和一组固定数值逐一和后续版本对比。注意初始时C矩阵要填入非零随机值这样才能验证beta分支没有被写坏。3.2 第二步分块与缓存优化让数据在缓存里“多活一会”下一步是把三重循环改成分块循环。外层按块遍历M、N内层按块遍历K每次计算一个C小块。void gemm_blocked(const float* A, const float* B, float* C, int M, int N, int K, int MB, int NB, int KB) { for (int m0 0; m0 M; m0 MB) { for (int n0 0; n0 N; n0 NB) { int mEnd min(m0 MB, M); int nEnd min(n0 NB, N); // 初始化 C 小块 for (int m m0; m mEnd; m) for (int n n0; n nEnd; n) C[m * N n] beta * C[m * N n]; for (int k0 0; k0 K; k0 KB) { int kEnd min(k0 KB, K); for (int m m0; m mEnd; m) { for (int n n0; n nEnd; n) { float sum 0.0f; for (int k k0; k kEnd; k) { sum A[m * K k] * B[k * N n]; } // 注意这里不能直接写回 C需要把临时结果累积起来 C[m * N n] alpha * sum; } } } } } }分块之后K的内循环被限制在KB以内。A的一个MB x KB块和B的一个KB x NB块会被加载到L1缓存然后在计算中反复命中。相比朴素版本这里的命中率已经有质的飞跃。不过这个版本有个明显问题C的累计是每次K块结束后做的这意味着C数据会被反复写回再读取。真正的高性能实现会把C小块保留在寄存器里最后一次性写回。这引出了微内核。这一阶段性能提升的典型幅度在形状为512x512的情况下是朴素版本的几倍到十几倍不等。关键观察点是分块变大时性能会先升后降这就是前面说的资源和复用之间的博弈。3.3 第三步引入微内核把C碎片钉在寄存器里微内核也就是microkernel是整个GEMM优化的核心。它的思想很简单取消每次K块结束时的C写回让一个“小块”的乘累加操作完全在寄存器里完成。// 计算一个 C 的 MB x NB 碎片寄存器内累加 void microkernel(const float* A_ptr, const float* B_ptr, float* C_ptr, int K, int ldC, int MB, int NB) { // 假设 MB4, NB4 float c[4][4] {0.0f}; for (int k 0; k K; k) { for (int i 0; i 4; i) { float a A_ptr[i * K k]; for (int j 0; j 4; j) { c[i][j] a * B_ptr[j * K k]; } } } for (int i 0; i 4; i) for (int j 0; j 4; j) C_ptr[i * ldC j] alpha * c[i][j]; }这段伪代码表达的核心思想是内层循环中A的一个元素被用于更新C一整行的4个寄存器累加器B的一个元素被用于更新一整列的4个累加器。数据从内存读到寄存器后被反复使用了4次。这就是数据复用从“缓存级”下沉到“寄存器级”。寄存器级复用的收益远大于缓存级因为寄存器的读写零延迟、零带宽消耗。微内核大小的选择也就是MB和NB具体取多少取决于寄存器数量。一个4x4的C碎片占16个寄存器加上A、B的向量寄存器以及地址计算、循环计数器总共可能占30到40个寄存器。一个CPU核通常有32个倍数关系的寄存器组4x4在大多数平台上都算安全。如果是支持更宽向量寄存器的平台6x8、8x8这类碎片也常见。核心原则是在编译器不溢出寄存器的前提下C碎片尽可能大。怎么判断溢出把性能测试时的汇编或者栈访问情况看一遍如果出现很多内存load/store说明寄存器不够用了。注意内循环里计算地址时尽量不要用数组索引里带乘法的形式比如A_ptr[i * K k]每次都会多一次乘法指令。在真正的内核里我会提前把指针步长算好用增量指针的方式把这些乘法消掉。这是编译期优化的基础也是很多伪代码移植到实际代码时性能低一截的原因。3.4 第四步多线程与硬件指令把多核和宽寄存器用满微内核本身是单核的优化要发挥多核优势还需要把分块循环拆到多个线程上。做法是按M或N方向做工作划分。我的习惯是按N方向划分因为这样不同线程访问的B列互不相交写C时不需要加锁而A会被多个线程共享读取这正好在缓存共享上有利因为同一块A数据不需要每个核都去主存读一次。每个线程分到一个或多个M_BLOCK x N_BLOCK的C块然后以自己的节奏跑完整套分块循环。注意控制线程之间的同步点最好只在每个工作块结束时同步一次不要在K块内部频繁插入屏障否则同步开销会吃掉所有性能收益。硬件指令的使用则因平台而异。支持SIMD的CPU上内层循环会把4个B的值装入一个向量寄存器跟广播后的A值做向量乘加支持张量核心单元的硬件上一次硬件乘加能完成一个矩阵碎片的运算。无论哪种微内核的基本结构都不变只是“最内层的乘加”由编译器或者硬件指令来加速。到这一步一个能打的GEMM内核已经成型了。我实测下来从朴素版到多线程微内核版在512x512形状上性能提升通常能到20倍以上已经非常接近硬件峰值的一个可观比例。剩下的工作就是针对目标形状调参数。3.5 关键参数实测选择一组肉眼调优的经验值分块参数最怕“看起来合理”实际性能拉胯。以下是我的起步参数和实测观察你可以在此基础上做tuning但不要上来就追求极致场景M_BLOCKN_BLOCKK_BLOCK微内核大小说明大矩阵均衡6464164x4通用安全大M大N12864326x8需要寄存器充足M较小3264164x4避免空洞INT8量化6464328x8整数乘加更轻实践里有个重要的调试技巧控制变量法。每次只改一个参数看性能数据别同时乱调。我见过太多人一上来把四个参数全部换掉性能变差了都不知道是哪个参数的问题。画一条“块大小-性能”曲线能很快找到你的平台性能拐点。4. 工程落地数据布局、融合算子和量化支持4.1 数据布局优化先转置还是当场转置朴素GEMM里C的计算需要A的每行和B的每列。内存布局如果按行优先存放B的列访问就是不连续的每一次取B[k * N n]都会引发缓存行浪费。高性能GEMM通常通过“打包”解决在进入微内核前把B的对应小块拷贝到一个连续的内存缓冲区里让内层循环访问B时完全是顺序的。A也可以做同样的处理甚至在拷贝过程中完成数据类型转换和转置减少内层的复杂分支。打包的开销是一次额外的拷贝但因为拷贝之后是连续访问缓存命中率高整体开销远小于它在内层循环里节省的访存损失。打包缓冲区要按块大小对齐最好一次性分配好避免反复申请释放。这一步看似简单实际对性能影响极大。我测试过不加打包的微内核版本性能会掉20%到30%。经验打包方向也有讲究。通常先打包B再打包A收益更高因为B的列访问模式是“完全随机”而A的行访问在行优先布局下本来就有不错的空间局部性。如果你只愿意先优化一个打包B优先。4.2 算子融合省掉中间张量的几个技巧推理引擎里GEMM后面几乎总是跟着一系列简单算子。与其让每个算子独立读写一遍全局内存不如把后置操作直接塞进GEMM的尾端。比如BiasAdd在微内核写回C之前的标量加法。ReLU / GELU在写回C寄存器时直接应用激活函数。残差连接把残差输入的加法和激活函数一起融合。量化如果GEMM输出后面紧跟一个量化节点那么反量化、应用scale、取整这些操作也可以在寄存器里做掉省掉中间FP32张量的存储和读取。注意融合要分层次。有些融合适合在微内核尾端做比如C碎片写回前有些融合更适合在分块外层做比如需要跨行处理的LayerNorm。不要把一切融合都塞进微内核否则寄存器压力过大反而得不偿失。判据很简单尾端融合只做“逐元素”操作任何依赖一整个向量或一行的操作都放到内核外面。4.3 量化GEMM整数算术的溢出与控制量化GEMM的核心公式是real_C scale_a * scale_b * (A_int B_int) ...通常会用A_int乘以B_int然后累加到INT32累加器里步进为scale_a * scale_b和zero point的修正。实现时最大的坑是溢出。两个INT8相乘最大是16256累加K次后K超过约131倍就可能冲破INT16的范围所以累加器至少要用INT32。更严格来说如果K很大还要评估是否需要用INT64累加或者把K维度拆成多段再合并。另一个坑发生在alpha和beta上。量化GEMM里alpha是浮点目标尺度beta是对旧C的尺度。不要每一步都做浮点乘法和反量化正确做法是先在整数域算完整个A B最后一次性应用scale和zero point。这样既减少了误差也减少了指令。如果你做的是FP16/FP8混合精度GEMM核心结构相同只是累加精度和异常处理对INF/NaN的屏蔽策略不同。我的建议是把量化参数包装成一个小结构体在微内核外部传进去内核里只用基础类型便于编译器做指令选择。4.4 与推理框架的集成shape推断与kernel选择GEMM内核做好之后要考虑怎么接入整个推理链路。一个独立的GEMM函数是一次性解决所有形状这在框架里并不好用。更合理的是做一个按形状分发的前端逻辑根据M、N、K、数据类型、对齐条件选择对应的内核实例。关键细节是充分性判断。比如M小于某个阈值时走专门的“小M”内核K特别大但N很小时可能应该走转置路径M和N能够被块大小整除时走“无边界检查”的快速路径不能整除时走“带边界检查”版本。这个分发层虽然不产生浮点运算但能省掉大量不必要的分支判断。有了它后续做自动调优就方便了。5. 常见性能问题与调试技巧5.1 一个指标表FLOPs、有效带宽、延迟、占用率怎么测先约定怎么衡量GEMM内核的好坏否则谈优化就是空谈。我每次调内核一定会记录下面几个指标指标公式或含义典型观察点实际FLOPs2MN*K / 耗时对比硬件理论峰值运算效率实际FLOPs / 理论FLOPs达到50%以上算合格有效带宽读取写入的数据量 / 耗时是否接近内存峰值带款指令效率有用指令 / 总指令用性能分析工具看stall分布寄存器溢出栈上是否出现频繁存储看汇编或性能计数器线程利用率并行活跃线程 / 总启动线程是否有等待/空闲性能分析工具不是玄学一定要用起来。它会告诉你内核到底卡在访存延迟、计算吞吐还是同步等待上。这三者的优化方向完全不一样访存瓶颈就去调分块和打包计算瓶颈就去查指令选择和循环展开同步瓶颈就去改任务划分粒度。5.2 性能低于预期的几个根源第一个根源是缓存未命中。表现为实测FLOPs远低于纸面预期但有效带宽已经接近硬件上限。这时候要检查分块大小特别是K_BLOCK是否已经超过L1缓存容量。把K_BLOCK从64降到16往往立即见效。第二个是bank冲突或通道冲突。在并行访问内存时多个线程访问同一bank会导致排队。表现在性能曲线上是某个块大小突然掉坑。解决办法通常是给B的打包缓冲区填充一点padding比如在每一行末尾多留4字节打破对齐。第三个是同步过频。有些实现喜欢在每个K块之后加线程屏障导致所有线程在每轮K循环互相等待快的那部分被慢的拖住。正确做法是尽量让线程独立工作更长的时间只在计算完整个C块后同步一次或者干脆按任务队列动态分配。第四个是任务粒度不均衡。M和N无法整除块大小时边缘块的计算量不满分配到这些块的线程会提前结束拖慢整体耗时。解决方法是把剩余边缘块合并成“单线程顺序执行”的特殊内核不与主内核复用。5.3 数值精度异常排查性能搞定后最烦人的就是数值错误。常见的坑有三个。第一累加顺序问题分块之后每个C块累加K的顺序和朴素版本不同浮点结果会有微小差异。解决方案不是消除差异而是设置合理的容差阈值。我在测试里用spectral norm或者随机向量的相对误差判定阈值设在1e-5FP32和1e-2FP16左右。第二alpha和beta的应用时机。在分块版本里如果你在K循环内部就先乘了alpha再在最后写回那么alpha会被应用很多次数值偏差会随K增大而累积。正确做法是alpha只在最后一次写回时应用。第三INT8的zero point没对齐。量化GEMM里zero point的修正项经常会被忘掉。特别是当A和B的zero point都不为零时需要额外减去A_zero * B_zero这一项。这是个极易出错的部分。建议单独写一个“零矩阵”测试把输入全部设为zero point对应的整数输出应该正好等于0附近的残差。5.4 六个实用调试口诀这六个口诀是我做了这么多年算子的总结对新手尤其有用先正确后优化。所有优化都基于一个验证过的朴素基线。要想快看访存。性能瓶颈十有八九先查数据搬运。向量化要写清。不要指望编译器自动把标量循环变成向量指令。寄存器别溢出。看见栈操作先检查分块大小。循环展开要适度。展开能提升指令级并行但也增加指令缓存压力。抓大头别抠小点。先把分块、打包、融合做了再想milestone级别的优化。6. 深度扩展DeepGEMM还能做什么6.1 稀疏GEMM与结构化剪枝模型剪枝之后权重矩阵会变成稀疏矩阵。传统GEMM会把大量零值也参与计算浪费算力。结构化稀疏GEMM的思路是把矩阵按块稀疏化比如2:4稀疏也就是每四个元素里保留两个。这类GEMM的核心是在普通GEMM的微内核上增加稀疏掩码处理硬件支持时可以直接跳过零值块。自己做的时候一个比较务实的起点是先做CSR或BSR格式的稀疏GEMM用分块的方式避免索引查找的开销。稀疏GEMM的加速比和稀疏度强相关越稀疏收益越大但实现复杂度也越高。6.2 自动调优autotuning空间手工调参是有上限的更系统的做法是引入自动调优。具体做法是把分块大小、微内核大小、循环展开因子、线程数这些参数枚举成一块“配置空间”针对目标硬件跑一遍记录每个配置的耗时选出最优配置。配置空间通常有几百到几千个组合用全枚举比较耗时但推理引擎本来就是针对固定模型做编译一次性成本可以接受。做自动调优要注意两点一是基准测试本身要足够稳定最好多次运行取最小值避免频率波动和后台进程干扰二是不要只调GEMM要联动后续融合算子一起调否则局部最优不是全局最优。我见过几次因为只调GEMM本身结果融合后的整体延迟反而变差的情况。6.3 多项式融合与图优化更高级的扩展是把多个GEMM和它们周围的算子整体融合。比如Attention里Q K^T和后面的softmax、 V能否在同一个内核里完成理论上可以但实现难度非常大因为softmax的softmax分母依赖整个行的最大值和求和这要求跨线程归约。一个折中的方案是融合QK^T和softmax的一部分或者把LayerNorm融合到前一个GEMM的尾端。这是推理引擎最能出成果的地方。同样的模型结构一个好的图优化管线可以把延迟砍掉两到三成而GEMM本身可能只能贡献其中两三成。DeepGEMM的核心内核做好了后续融合就等于在已有的“高速计算单元”上附加几乎零开销的周边操作这比重新写一整个新算子要省力得多。我自己在这套项目上踩过最深的坑其实是过度优化。有一阵子为了追求单内核的极致效率先是把汇编调得非常复杂然后又加了各种特殊分支结果边缘情况下性能不稳定代码也难维护。后来我把大部分特殊分支移出去留下一个相对简洁但稳定的内核再靠自动调优找参数整体反而更快。高性能计算里代码的简洁性和运行效率往往是一体两面——复杂的分支会让指令缓冲失效也会让编译器更难优化。这个教训做任何算子库都适用。如果你也想从零开始做一个类似的GEMM我的建议是先别碰复杂硬件指令把一个分块微内核调明白再加上多线程和打包这一套已经能让你对高性能计算有非常完整的理解。等你真的需要冲峰值时再到汇编层面去打磨你会发现之前打下的基础让一切都顺理成章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑