某技术社区刷到这个标题的时候我正对着一个跑不出预期性能的算子发愁。AI 能写出更快的 kernel编译器就该退场了——这句话单看前半段确实戳中了不少人的兴奋点但后半段怎么说呢有点像看到个新来的实习生干活利索就断言老师傅可以提前退休了。作为常年跟 kernel 优化、编译器后端打交道的人我觉得这个话题值得认真拆一拆因为里面既藏着 AI 辅助编程的真实价值也混着不少对编译器本质的误解。先说结论放在前面AI 确实已经能写出在某些场景下比人工更快的 kernel 代码但编译器不但不会退场反而会在 AI 介入之后变得更加重要。这篇文章我打算从 AI 写 kernel 的实际能力边界讲起再回过头说说编译器在现代芯片上到底做了哪些“看不见的苦力活”最后用一次完整的调优实操来对比人工、AI、编译器三者各自的角色顺便把我踩过的坑和排查心得一并交代清楚。不管你是做算子开发、上层应用优化还是只是对“AI 替代程序员”这个话题感兴趣这篇应该都能给你一个相对扎实的参考视角。1. AI 写 kernel 这件事到底靠谱到什么程度1.1 AI 擅长的其实是“语法正确的代码”而不是“性能正确的代码”我一直在用 AI 辅助写高性能计算相关的代码包括 CUDA kernel、CPU 上的 SIMD 优化版本以及一些异构平台上的加速实现。一个很直观的感受是AI 在生成“语法正确、逻辑基本没毛病”的代码方面水平已经相当可以了。比如你给它一个明确的任务描述对一张 1024x1024 的浮点矩阵做转置要求按 16x16 分块避免 bank conflict它能直接给出一个能跑、结果对的 kernel甚至分块大小、共享内存声明、索引计算这些细节都不会出错。这在两年前是不太敢想的。但这里有一个关键区别逻辑正确只是第一步kernel 开发的真正难点从来都是“性能正确”。所谓性能正确是指代码在目标架构上真正把硬件资源吃满了——访存带宽、指令流水线、寄存器压力、缓存命中率、指令级并行度这些指标得一起达标才算数。AI 目前在这方面的表现更像是“读过很多武林秘籍但内力还不够深”的少年它知道套路但不太擅长判断在特定硬件上哪个套路真正管用。我做过一个很简单的实验让 AI 生成一个矩阵乘法 kernel不限定任何优化策略。它给了我一个很标准的 tiled 版本用了共享内存分了 16x16 的 tile逻辑严谨。但同一块卡上一个手写调优过的版本用上了 8x8 的 warp tile、双缓冲、向量化加载和寄存器重排性能差距接近 3 倍。AI 写的那版没错只是不够快。这就像你会做番茄炒蛋跟你能在米其林后厨高压环境下三分钟出一道完美的番茄炒蛋中间隔着的是一整个系统性的调优认知。1.2 “更快的 kernel”里藏着一大堆 AI 没见过的东西既然说 AI 能写出更快的 kernel那我们就得先定义清楚“更快”到底意味着什么。在不同场景下这个目标的差异大到像两个完全不同的任务。在 CPU 上kernel 的性能可能取决于向量化是否完整、cache 行是否被好好利用、分支预测是否友好。在 GPU 上则要操心线程块大小、共享内存占用率、访存合并、warp 调度效率。在 AI 芯片上可能还得考虑数据在片上 SRAM 和主存之间的搬运策略算子与算子在流水上的排布方式。这些硬件层面的约束每一个都包含大量的架构手册细节和实测调优数据AI 的数据训练集里未必有足够覆盖。举一个更具体的例子在 ARM 平台上有一种叫 SVE 的向量指令集它和常见的 SSE/AVX 最大的不同是向量长度不固定由硬件决定。生成针对 SVE 的 kernelAI 很可能按照 AVX 的思路写成固定 512 位宽度的假设结果在真实硬件上要么编译失败要么根本无法发挥可伸缩向量长度的优势。这种“看起来像、其实架构底层完全不同”的细节是 AI 最容易翻车的地方。那是不是说 AI 不行也不是。我的实际体验是AI 更适合作为“思路生成器”和“脚手架搭建器”它能帮你快速铺出一个正确的基础实现但真正的性能提升仍然需要人或者说某种更底层的机制来完成。这就是编译器该登场的地方。2. 编译器做的事情远比“翻译代码”这四个字复杂2.1 从 IR 到指令调度编译器是个多层决策系统很多人提到编译器脑子里第一反应是“把高级语言翻译成机器码”。如果编译器真的只是做翻译那 AI 确实可以用更聪明的代码生成轻松取代它。但现实是现代编译器是一个极其精密的多层优化决策系统翻译只是它最外围的工作。以 LLVM 为例代码先被转换成中间表示IR然后在 IR 层面完成大量平台无关的优化——比如循环不变式外提、死代码消除、函数内联、全局标量替换。这些优化完成后IR 又被下探到目标相关的层开始针对特定 CPU 微架构做指令选择、寄存器分配、指令调度、向量化重组。每一个环节背后都是几十年编译原理研究和工程实践的积累。关键点在于编译器手头掌握的信息是 AI 生成代码时没有的。AI 看到的是代码文本本身它不知道这段代码会被跑在什么微架构上、不知道循环实际会迭代多少次、不知道内存访问模式在运行时是否有变化。而编译器在做 PGO按配置文件优化的时候是可以拿到运行时的执行频率信息的可以据此调整分支布局、内联决策、循环展开因子。这种“知道代码将如何在真实硬件上奔跑”的能力AI 目前不具备。2.2 你以为你在手写优化其实你写的是给编译器看的“优化提示”这可能是我从业这些年最深刻的体感之一你在 kernel 代码里写的很多“优化”本质上不是在直接告诉硬件怎么做而是在给编译器的优化管线提供线索。举个例子你在 CUDA kernel 里手动做循环展开让相邻两次循环体的计算天然形成指令级并行的空间。但现代编译器通常会在后端自动做循环展开和指令重排。你手动展开的版本经过编译器一通操作之后和原本没展开的版本在性能上经常没有本质区别。反过来有时候你手动写的展开反而打乱了编译器原有的向量化节奏造成负优化。我在某个项目里干过一件很蠢的事花了整整两天手写了一个 AVX-512 版本的数组求和 kernel精心安排了寄存器分配和加载时机自认为已经把待机延迟完全隐藏了。结果同事提醒我直接在普通循环上开-O3 -marchnative编译器自动向量化的版本性能比我的手写版快了 12%。原因并不复杂编译器做了比我更精细的软件流水把多个迭代的重叠度算得更好而且它还根据硬件反馈选择了不降低时钟频率的指令混用方式。这说明一个残酷的事实在编译器高度成熟的今天手写优化的空间正在被挤压但这不是 AI 的功劳而是编译器本身在持续进步。AI 生成更快的 kernel 这件事很多时候是它在模仿编译器做过的事而不是超越编译器。2.3 编译器完全可以被当成一个“调参工具”而 AI 的用武之地在这之上既然编译器这么强那还有没有 AI 发挥的空间当然有。我认为 AI 真正的价值体现在两个层面一是帮助生成更符合编译器预期的代码二是在编译器给不出最优解的试探性问题上提供新的方向。所谓“更符合编译器预期的代码”指的是写代码时就按照编译器优化管线的偏好来组织逻辑。比如尽量让循环保持简单的计数结构方便编译器做向量化分析避免循环内部出现可能指向同一个内存区域的别名方便编译器做重排把不变量提到循环外面去减少编译器的工作量。AI 经过大量高质量代码训练后在这些方面通常比新手做得更好。第二个层面更有意思。有些性能问题在编译器框架内是无解的——编译器受到编译时间的限制不可能对所有循环穷举所有优化策略。这时候AI 生成的“非常规”代码方向反而能提供启发。比如某次我在一个图像处理 kernel 上试了 AI 提出的一个内存布局方案不是传统的 HWC 也不是 CHW而是把通道维度做了分组交错。这种布局既不符合人类直觉也不在编译器任何预设模式里但实测确实减少了 cache miss。没人说 AI 不能干这种事但它干成这种事依赖的前提仍然是有一个足够强大的编译器在底层做兜底把 AI 那些不太常规的意图正确翻译成高效的机器指令。这也是我一直坚持的观点AI 和编译器是上下楼的关系AI 在楼上提思路、定结构编译器在楼下落实、做脏活累活。想让 AI 直接取代编译器等于想让建筑师取代施工队——蓝图画得再好没人捆钢筋浇混凝土楼是立不起来的。3. 一次实际调优实验人工、AI、编译器三方角力3.1 实验场景与初始性能摸底光讲道理没意思下面用一个实际做过的实验来演示三者的真实表现。模拟项目 X 的需求是优化一个 3x3 卷积 kernel作用于 512x512 的单通道灰度图运行平台是某款常见 x86 CPU编译器用的是 Clang开启-O3 -marchnative。这是个典型的内存密集型算子计算量不大但访存量不小非常考验数据复用和访存优化。先跑了一个最朴素的版本两层循环遍历输出像素每个输出像素读取 3x3 邻域做乘加。性能数据耗时约 18.6 毫秒。作为基准这算是“能跑但没任何优化”的状态。接下来用 AI 生成一个优化版本。我给它的提示是用行缓存line buffer的方式减少重复行的加载尽可能利用 CPU 的缓存层次并考虑向量化处理。AI 给了一版带三行缓存、用_mm256做水平方向向量化的实现。这个版本跑出来的结果是 6.2 毫秒比朴素版快了整整 3 倍。到这里如果只看结论确实可以喊一句“AI 写得更快”但且慢——真正的调优竞赛才刚开始。3.2 当编译器在相同思路上“自动”跑了一遍之后我没着急把 AI 的版本直接当最终答案而是做了一件更刁钻的事把 AI 代码里手写的行缓存逻辑去掉换成一种更直观但内存访问上等价的结构然后丢给编译器自己优化看看它能不能“悟”出类似的东西。结果有点戏剧化。Clang 在-O3 -marchnative下对那个看似朴素、但内存访问模式相对规整的循环做了自动向量化还配上了循环展开和访存合并最后的耗时是 5.7 毫秒比 AI 精心设计的手写版本还快了 8%。原因在于AI 手写的_mm256向量化代码对内存对齐有严格要求但它的实现里没有显式做对齐处理导致编译器不得不生成一段额外的对齐检查分支反而拖慢了主循环。这个实验的结论很有代表性AI 能想到用行缓存这一步体现了它的知识迁移能力值得肯定但编译器在底层对指令选择和访存细节的处理仍然强过 AI 的直接生成结果。AI 写出“思路正确”的代码编译器把它们打磨成“执行高效”的代码两者配合才是最优解。3.3 人工调优的不可替代之处知道什么时候不该信编译器最后上场的是人工调优。我花了一个晚上手工排查这份 kernel 的瓶颈用性能分析工具发现耗时主要集中在加载像素数据时的地址计算上——每次迭代需要做一次二维到一维的索引换算产生了额外的整数乘法。这个开销是编译器看不出来的因为它觉得那只是几条整数指令但实际在流水线上挤占了不少发射槽位。手动做了一个小改动把三层循环重写成针对每一行只计算一次基地址、然后三个内层循环用步长递增直接访问的形式。索引换算从每像素一次变成了每行一次。改动不大但省下来的整数运算让总耗时降到 4.1 毫秒。这一步AI 没提出来编译器也没自动做完全依赖人对硬件流水线和地址计算开销的直觉。到这里三者的对比就很鲜明了方案耗时毫秒相对基准加速比说明朴素实现18.61.0x纯粹能跑的版本AI 生成优化版6.23.0x思路正确细节拖分了编译器自动优化5.73.3x在“编译器友好的代码”上表现很好人工调优版4.14.5x基于硬件微观结构做了定向优化顺带说一句最后我还把人工调优版的代码喂给 AI问它能不能继续优化。它倒是老老实实说这已经接近内存带宽理论上限了再优化得换算法思路比如用 FFT 做频域卷积。这个回答挺诚实也说明 AI 已经具备一定的性能评估意识——但要让它自己意识到前面那几步的优化空间目前还做不到。4. 常见误区和踩坑实录关于性能优化和 AI 代码的几条真心话4.1 误区一AI 生成代码的性能可以直接信最危险的坑就是拿到 AI 生成的 kernel 跑了一下发现比原来的快就当作最终版发布。我见过不止一次因为这个操作导致线上性能反而回退的情况。原因不复杂AI 生成的代码经常会在某个特定输入大小下表现优异但换个 shape、换个 batch size内存访问模式变了缓存行为全变性能就从天上掉到地下。正确的姿势是把 AI 生成的代码当成一个候选方案必须在不同输入规模、不同数据布局下做充分的性能回归测试。最好在测试集里加入边界条件比如宽高不是 16 的倍数、矩阵不是连续的、数据对齐在奇数地址上。我通常会写一个简单的自动化脚本跑一系列 shape 下的耗时对比全部过了才敢进主线。这件事说白了就是AI 帮你写好了代码但你得帮它写好“验证条件”否则它的优秀表现可能只是一次性的。4.2 误区二编译器优化全靠-O3 -marchnative就完事了很多人会把编译选项当成一个“一键加速”按钮觉得开了-O3 -marchnative就是压榨完硬件性能了再慢就是算法问题。这个认知对一半。-O3和-marchnative确实是重要的基础优化但真正吃性能的场景还需要更多精细控制。我常用的一套组合包括-flto让跨编译单元的函数内联和常量传播生效-fno-math-errno在允许的情况下让浮点运算走更快的无错误处理路径-fprofile-generate加-fprofile-use做 PGO让分支预测和函数内联决策基于真实运行数据。还有一个经常被忽略的是-mtune它比-march更进一步针对特定微架构的流水线深度和调度策略做调优。这些选项叠加在一起同一个 kernel 的性能差距可以再拉开 15% 到 30%。但它们的使用也是一把双刃剑比如-fno-math-errno会改变 NaN 和无穷值的处理语义如果代码里依赖这类行为就会翻车。我在一个数值计算项目里就因为这个选项被一个隐蔽到极点的 bug 坑过输入数据里混入了 NaN本应传播到输出结果以便上层发现异常结果这个选项让 NaN 变成了一个默认值异常被静默吞掉了排查了整整两天。4.3 误区三内存带宽是瓶颈所以看什么都是访存的锅性能分析里最常见的偷懒操作是一看 profile 访存时间占比高就归因到内存带宽然后停止深挖。这个判断错得不冤但确实太粗糙了。访存时间长可能是带宽真的打满了但也可能是缓存命中率太低导致的延迟暴露、TLB miss 过于频繁、访存指令的发射队列拥堵、甚至相邻线程之间的 cacheline 伪共享。正确的分析方式是用硬件计数器来区分这些可能性。perf stat可以给出 L1 命中率、L2 命中率、LLC 命中率、TLB miss 次数等一堆指标。比如有一次我碰到一个 kernelLLC 命中率明明很高但性能就是上不去。深入排查后发现问题出在 TLB 上——数据是随机访问的每页只踩到一点点导致 TLB 条目频繁被换出。解决办法也够奇葩改用大页内存TLB 覆盖范围一下子扩了 512 倍耗时直接砍掉四成。这种排查过程AI 目前是不可能自动帮你完成的因为它看不到硬件的计数器数据也无法理解运行时行为。它顶多建议你“尝试调整缓存友好性”但具体是调整数据布局还是改访存顺序它给不了精确答案。而编译器呢——它能自动做一些缓存分块优化但它并不知道你分配内存时用没用大页所以还得靠人靠 profiling 工具把线索串起来。4.4 我逐渐摸索出的一套工作流经过这些小插曲我现在做 kernel 优化的大致套路是先用 AI 快速生成一个基础版本跑通逻辑和正确性然后把它扔给编译器在-O3 -marchnative基础上叠加各种选项做一个基础性能基线扫描之后用性能分析工具定位真正的瓶颈是在访存、指令调度还是并行度不足最后针对瓶颈做定向调整可能是改数据结构可能是改循环结构也可能是调整内存分配策略。AI 在这个流程里的角色更像是第一天来报到的高级实习生——理论基础扎实动手也快但对你们组的历史包袱、线上奇葩 case、硬件平台的暗坑一无所知。你可以让它去画第一版方案、做繁琐的 boilerplate 代码但最终拍板、兜底、上保险丝的还是人。编译器呢它像是那个任劳任怨的老师傅平时不声不响但你交给它的每一行代码它都会认真对待在指令级别替你着想。4.5 给想试试 AI 辅助 kernel 开发的朋友几点实操建议如果读到这里你也想上手试试我建议从三个方向开始一是选好参照系。不要拿 AI 生成的代码根朴素实现比那样比出来的加速比没有意义。要拿它跟-O3编译器的自动优化结果比这才是 AI 真正需要证明自己的地方——它能不能超过一个充分优化的编译器。二是做好正确性验证。AI 生成的 kernel 里边界条件处理是重灾区。典型的翻车点是循环上界差一、数组末位越界、数据类型隐式转换导致精度丢失。我的习惯是构造一个输入规模从小到大的测试矩阵每个规模都跟参考实现做逐元素比对同时监控最大值和总和的相对误差。三是保持“调优理性”。AI 给的优化建议不要照单全收。让它解释每一项优化为什么有效有效的前提条件是什么在什么输入下会失效。如果一个建议它解释不清楚大概率是从训练数据里拾人牙慧不是真正的理解这种建议要打折扣。5. 编译器退场这件事更像是一个伪命题把话题拉回标题提出的那个问题。我认为“AI 写 kernel 更快所以编译器退场”的推理链条从一开始就存在两个脆弱环节第一AI 生成的代码本身需要经过编译器的降级和优化才能真正落到硬件上执行这个环节没有编译器会出现灾难性的工程问题第二AI 目前展示出来的“更快”往往是在窄场景、固定输入条件下取得的面向真实世界中千变万化的算子组合它还没有足够的泛化能力。更重要的是编译器不是一段只为了生成机器码的静态工具它是一个持续吸收硬件特征、指令集演进、优化策略新成果的开放系统。就算 AI 把整个 kernel 的代码全部包办最终还是要有人把这些代码放到一个可以持续迭代、性能可复现、回归可验证的工程环境里而这个环境从来都离不开编译器。我个人在实际操作中的体感是AI 确实帮我省去了大量写样板代码的时间但真正把 kernel 从能跑拉到最优的那个 10-20%靠的还是对硬件细节的执拗、对性能分析工具的耐心、对不同优化选项组合的反复试验。AI 可以压缩我“从一个想法到一个可运行版本”的时间但它并没有压缩“从可运行到高性能”的那个过程——那段路依然需要编译器这个精密而可靠的伙伴。如果你现在是一个刚接触 kernel 开发的工程师我会建议你别抱着“让 AI 一步到位”的期待也别忘了-O3之外还有无数编译器选项在等你认识。把 AI 当辅助把编译器当基本功把 profiling 当眼睛三者配合出来的结果大概率比单一依赖任何一方都好得多。最后再分享一个小技巧当你调不动性能的时候试着去掉两三行看似必要的优化代码让编译器“呼吸”一下有时候它反而会给你一份意外之喜。