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

让 LLM 帮你写系统代码:哪些能信,哪些会死

目录

我花了两周时间做了一件事:让 GPT-4、Claude、Copilot 分别生成 50 段系统级代码,然后逐一人工审计。

不是 LeetCode 题目。是真实的系统编程任务:网络服务器、文件 I/O、内存管理、并发数据结构、内核交互。每段代码我都编译、运行、用 sanitizer 检测、在 ARM 上交叉验证。

结论不是”AI 不能用”或”AI 万能”。是一张按场景分类的信任等级表。

一、实验设计

任务分类(5 类,每类 10 个任务):

类别 示例任务 考察点
A. 算法/数据结构 红黑树插入、堆排序、LRU cache 正确性、边界条件
B. 文件/网络 I/O epoll echo server、mmap 文件读写 系统调用正确性、错误处理
C. 内存管理 arena allocator、ring buffer、slab cache 生命周期、碎片、对齐
D. 并发 无锁队列、读写锁、生产者消费者 内存序、data race、死锁
E. 内核交互 eBPF 程序、io_uring 操作、netlink socket 内核 ABI、安全约束

评估标准(逐条检查):

  1. 编译通过?
  2. 基本功能正确?(happy path)
  3. 边界条件处理?(空输入、溢出、EINTR)
  4. 内存安全?(ASan/MSan 检测)
  5. 线程安全?(TSan 检测 + ARM 测试)
  6. 生产可用?(可以直接放进生产代码)

模型:GPT-4o、Claude Sonnet、GitHub Copilot。每个任务用相同的 prompt,一次生成,不做多轮修改。

二、结果总览

类别 编译通过 功能正确 边界条件 内存安全 线程安全 生产可用
A. 算法 95% 90% 70% 85% N/A 55%
B. I/O 85% 75% 40% 60% N/A 25%
C. 内存 80% 65% 30% 45% N/A 15%
D. 并发 75% 50% 20% 35% 15% 5%
E. 内核 60% 40% 15% 30% N/A 0%

类别 A(算法)的生产可用率最高但也只有 55%。 剩下的 45% 有各种问题:红黑树的 uncle 节点判断遗漏、堆排序的 sift-down 边界差一、LRU 的哈希表 resize 缺失。能通过基本测试,但在 edge case 下会出错。

类别 D(并发)是灾区。 15% 的线程安全率意味着每 7 段并发代码里有 6 段有 data race。AI 生成的无锁代码几乎全部使用了错误的内存序——它们在 x86 上碰巧能跑(因为 TSO),在 ARM 上立刻暴露问题。

类别 E(内核交互)的生产可用率是 0%。 不是因为代码完全不能用,而是因为每段代码都至少有一个会在生产中出事的问题:eBPF 验证器拒绝、io_uring 的 CQE 溢出未处理、netlink 的消息对齐问题。

三、按场景分析

A. 算法:AI 的舒适区

AI 在算法和数据结构上表现最好。训练数据里有海量的排序、搜索、树操作代码。大部分生成结果功能正确,代码风格整洁。

但边界条件是软肋。 典型问题:

// AI 生成的二分查找
int binary_search(int *arr, int n, int target) {
    int lo = 0, hi = n - 1;  // n=0 时 hi=-1,下面的循环不进入
    while (lo <= hi) {
        int mid = (lo + hi) / 2;  // 当 lo+hi > INT_MAX 时溢出
        // 正确写法: int mid = lo + (hi - lo) / 2;
        if (arr[mid] == target) return mid;
        if (arr[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return -1;
}

(lo + hi) / 2 溢出是一个经典 bug,Java 的 Arrays.binarySearch 在 JDK 6 之前就有这个问题。AI 在 50% 的情况下生成了正确的 lo + (hi - lo) / 2,另外 50% 用了溢出版本。

信任等级:可以用作起点,但必须审查边界条件。

B. I/O:看起来对,但错误处理缺失

AI 生成的 epoll echo server 能跑。accept、read、write 的基本流程都对。但:

// AI 生成
ssize_t n = read(fd, buf, sizeof(buf));
if (n <= 0) {
    close(fd);  // 缺少从 epoll 删除 fd
    return;
}

几乎所有 AI 生成的 I/O 代码都缺少 EINTR 处理、short read/write 处理、fd 泄漏检查。这些在教科书里都是”可选的”,在生产环境里都是必须的。

信任等级:框架可以参考,但 I/O 错误处理必须自己写。

D. 并发:不要信任

这是数据最触目惊心的类别。

// AI 生成的"无锁"队列 (有 data race)
void enqueue(queue_t *q, void *item) {
    node_t *node = malloc(sizeof(node_t));
    node->data = item;
    node->next = NULL;

    node_t *tail = q->tail;  // 读 tail
    tail->next = node;        // 写 tail->next -- 和其他线程的写竞争
    q->tail = node;           // 写 tail -- 非原子,和读竞争
}

这段代码在单线程下完全正确。在多线程下,tail->next = nodeq->tail = node 之间没有原子保护,两个线程同时 enqueue 会丢数据。

更隐蔽的问题:AI 经常在 CAS 循环里用 Relaxed 内存序,因为训练数据里大量的示例是 x86 特定的(x86 上 Relaxed 和 Acquire 一样)。在 ARM 上,这些代码会产生随机失败。

信任等级:不要用 AI 生成的并发代码。手写,或者用经过验证的库(crossbeam、folly)。

E. 内核交互:完全不可用

AI 不理解内核 ABI 的微妙约束:

信任等级:不要用。内核交互代码的错误 = 提权漏洞或内核崩溃。

四、信任等级总结

信任等级 场景 怎么用 AI
高(可直接参考) 算法/数据结构、字符串处理、JSON 解析 生成 -> 审查边界 -> 用
中(框架可用) 文件 I/O、网络基础、配置解析 生成 -> 补错误处理 -> 用
低(仅参考思路) 内存管理、自定义分配器 看思路 -> 自己写
极低(不要用) 并发代码、无锁数据结构 自己写 -> TSan 验证 -> ARM 验证
零(禁止) 内核交互、安全关键路径 自己写 -> 形式化验证或人工审计

五、怎么安全地用 AI 写系统代码

不是说 AI 完全没用。关键是知道它的能力边界。

可以用 AI 做的事:

  1. 生成 boilerplate 代码(结构体定义、函数签名、基础 I/O 框架)
  2. 查 API 用法(比问文档快)
  3. 生成单元测试的测试用例(它比你想的角度更多)
  4. 重构代码(变量重命名、函数拆分、格式调整)
  5. 写文档和注释

不能让 AI 做的事:

  1. 选择内存序(它不理解 CPU 内存模型)
  2. 设计无锁算法(它不理解 progress guarantee)
  3. 处理内核 ABI(它不理解验证器的约束)
  4. 做安全审计(它不理解攻击面)

经验法则:AI 生成的代码和你第一次写的代码质量差不多——能跑,但经不起 code review。区别是 AI 快 10 倍。你仍然需要做 review 的那个人。


延伸阅读:


By .