土法炼钢兴趣小组的算法知识备份

AI 生成的'高性能'代码到底有多快

目录

AI 生成的”高性能”代码到底有多快

上一篇测了 AI 代码的正确性。这篇测性能。

我给 AI 三个明确的高性能任务:SIMD 向量排序、内存池分配器、epoll 事件循环。每个任务的 prompt 都强调了”高性能”、“零拷贝”、“缓存友好”等关键词。

然后我用 perf statcachegrindperf record 对每段代码做性能分析,和手写的优化版本对比。

一、SIMD 向量排序

任务:对 1M 个 int32 排序,要求使用 SIMD 指令。

AI 生成的版本

AI 生成了一个用 AVX2 做 8-element sorting network 的实现。代码结构正确:加载 8 个 int 到 __m256i,用 _mm256_min_epi32 / _mm256_max_epi32 做比较交换,然后做全局归并。

perf stat 结果:

指标 AI 版本 std::sort 手写 SIMD
时间 85 ms 62 ms 38 ms
IPC 1.2 2.1 3.5
L1 cache miss 18% 5% 3%
branch miss 2% 8% 0.5%

AI 版本比 std::sort 还慢 37%。用了 SIMD 反而更慢。

cachegrind 分析原因:

  1. 内存访问模式:AI 的归并阶段用了 malloc 分配临时数组,每次递归都分配。频繁 malloc/free 导致 cache 污染。手写版本用栈上固定大小 buffer,零分配。
  2. SIMD 指令选择:AI 用的是 128-bit 的 _mm_min_epi32,不是 256-bit 的 _mm256_min_epi32(虽然 prompt 说了 AVX2)。一半的 SIMD 带宽被浪费了。
  3. 归并策略:AI 做的是标准归并排序 + SIMD 小数组排序。手写版本用 SIMD 做 bitonic merge network,归并本身也是向量化的。

AI 知道 SIMD 的 API,但不知道怎么让数据流过 SIMD 管道。 它把 SIMD 当成”快速比较”来用,而不是重新设计整个算法让它 SIMD-friendly。

二、内存池分配器

任务:实现一个固定大小块的内存池,支持 alloc/free,要求比 malloc 快。

AI 生成的版本

AI 生成了一个 free list 内存池:预分配一大块内存,按固定大小切块,用链表串起来。alloc 从链表头取,free 插回链表头。

逻辑完全正确。但 perf 说:

指标 AI 内存池 glibc malloc 手写内存池
alloc (ns) 8.5 3.8 2.1
free (ns) 6.2 3.2 1.8
alloc+free 交替 (ns) 12.5 6.5 3.5

AI 的”高性能”内存池比 glibc malloc 慢 2 倍

为什么:

  1. 链表遍历:AI 的 free list 用的是指针链表。alloc 时取链表头(O(1)),但 free 时插入链表头涉及一次指针写入。问题是指针写入的位置和上次 alloc 的位置可能不在同一个 cache line,触发 cache miss。
  2. 对齐问题:AI 分配的块按 8 字节对齐,但 CPU cache line 是 64 字节。频繁分配小块导致同一个 cache line 被多个块共享,产生 false sharing(多线程场景下会更严重)。
  3. 分支预测:AI 在 alloc 里加了两个检查(pool 是否为空、块大小是否匹配),glibc 的 fastbin 在热路径上是零检查的。

手写版本的优化: - 用数组 index 代替指针(index 是 uint32,指针是 uint64,cache 利用率翻倍) - 64 字节对齐避免 false sharing - alloc 热路径编译为 3 条指令(load index, increment, return pointer)

AI 写的是教科书上的内存池。教科书不讲 cache line。

三、epoll 事件循环

任务:高性能 TCP echo server,要求优化吞吐量。

AI 生成的版本

代码结构正确:epoll ET 模式、non-blocking fd、read/write 循环。但:

指标 AI 版本 手写优化版
QPS (1KB msg, 1000 conn) 125K 185K
syscall/request 4.2 2.1
context switch/s 12,000 3,500

AI 版本比手写慢 32%。

strace 分析:AI 的代码每次 epoll_wait 返回后,对每个就绪 fd 做一次 read + 一次 write。手写版本做了两个优化:

  1. read 循环:ET 模式下,一次 epoll_wait 返回后要读到 EAGAIN,不是只读一次。AI 只读一次,导致同一个 fd 在下一次 epoll_wait 时又就绪了(多一次系统调用)。
  2. writev 合并:多个小写合并成一次 writev,减少系统调用次数。AI 对每个 fd 单独 write。

这些不是”高级优化”。这是 epoll ET 模式的基本要求。但 AI 的训练数据里大量的 epoll 示例是 LT 模式的教学代码,它把 LT 的用法搬到了 ET 上。

四、为什么 AI 写不好性能代码

三个案例的共同模式:

维度 AI 的能力 AI 的缺陷
API 正确性 知道 SIMD 指令、epoll 参数、链表操作 不知道 cache line、分支预测、系统调用开销
算法选择 教科书算法(归并排序、链表) 不考虑 cache locality 的算法
代码风格 整洁、可读 关注可读性而非性能
优化思路 知道”零拷贝”、“SIMD”等关键词 不知道怎么把这些技术和具体数据流结合

核心问题:AI 理解”做什么”(API 语义),不理解”怎么做快”(微架构行为)。 性能优化不是调用正确的 API——是让数据以正确的方式流过 CPU 管道。这需要理解 cache hierarchy、branch predictor、memory bandwidth、instruction-level parallelism。这些知识在训练数据里零散存在,但 AI 不能系统性地应用它们。

五、怎么用 AI 写性能代码

  1. 让 AI 写正确的第一版。不要在 prompt 里要求高性能——AI 会加不必要的复杂度(比如不需要 SIMD 的地方用 SIMD)。
  2. 你来做 profilingperf stat 看 IPC 和 cache miss,cachegrind 看热路径,perf record 看调用栈。
  3. 根据 profiling 结果手动优化。热路径通常只有 10-20 行代码,手动优化这些代码。
  4. 让 AI 帮你写测试和 benchmark harness。这是 AI 擅长的——生成大量测试用例和计时代码。

AI 是好的草稿工具,差的优化工具。让它帮你快速到达”正确但慢”的阶段,然后你接手优化到”正确且快”。


延伸阅读:


By .