发布时间:2025-02-18 14:06:59 点击量:
HASH GAME - Online Skill Game GET 300
在这个实验中,我们将给 Claude 3.5 Sonnet 一个面试风格的 Python 编码提示词:一个简单适合被新手软件工程师实现,但同时又可以进行大量优化的问题。这个简单随意的提示词代表了典型软件工程师使用 LLM 的方式。此外,测试提示词必须完全原创,而不是取自 LeetCode 或 HackerRank 等编码测试平台,因为 LLM 很可能在这些平台上进行过训练,可能会通过背诵记忆的答案作弊。
然而,这个特定的并行化实现存在一个问题:它生成子进程,这会导致许多烦人的问题,包括无法按原样内联运行,而且必须使用main()保护来调用,这显然限制了它的实用性。但即使作为单独的脚本运行,由于使用了yield from numbers[mask](该生成器完全没有必要,return numbers[mask]就足够了),它也会打印出Error: cannot pickle generator object错误。代码还混合了 numpy 数组的dtype,这会导致错误:将它们全部设置为np.int32可以修复这个问题。
现在是 2025 年,要从 LLM 那里获得最佳结果,提示词工程 (prompt engineering) 仍然是必需的。事实上,提示词工程对 LLM 变得更加重要:下一个 token 预测模型是通过在大批量输入上最大化下一个 token 的预测概率来训练的,因此它们针对平均输入和输出进行优化。随着 LLM 的显著改进,生成的输出变得更加平均化,因为这就是它们的训练目标:所有 LLM 都偏向于平均值。虽然这既违反直觉又不有趣,但少量的指导,明确告诉 LLM 你想要什么,以及给出一些你想要的例子,将客观上改善 LLM 的输出,远超过构建这些提示词所需的努力。Claude 3.5 Sonnet 由于其强大的提示词遵循能力,即使只是一点点提示词工程也能显著受益。
让我先规划优化: 1. 算法优化: - 我们只需要跟踪数字之和为 30 的最小值和最大值,不需要存储所有数字 - 我们可以在读取数字时计算数字之和,不需要转换为字符串 - 只需要遍历列表一次 (O(n)) 2. 内存优化: - 只需要存储 2 个变量(最小值和最大值),不需要完整的过滤列表 - 不需要额外的数据结构 3. 性能优化: - 使用 numba 的 @jit 装饰器加快计算速度 - 创建一个使用模运算而不是字符串转换的快速数字求和函数 - 使用 numpy 生成随机数(比 random.randint 更快)
很好,它识别出了parallel=True标志!但是那个数字求和实现很奇怪,使用了位移操作[15],这通常只用于超优化。我不认为你可以在十进制数上很好地使用位移操作,经过测试,事实证明确实不行,因为这个实现计算出的数字之和是错误的。该实现还包括了多进程分块方法(未显示),这可能与 numba 重复并造成额外开销。同样未显示的是:脚本还使用小型测试数组预编译了 JIT 函数以获得更好的实际性能,这是numba 文档推荐[16]的基准测试方法。
Claude 开始使用SIMD 操作[18]和块大小来实现(理论上的)极致性能。我对那个位移实现感到困惑,因为它仍然是错误的,特别是现在涉及到十六进制数。事实证明,这是一个计算十六进制数字的数字之和的优化方法,而不是十进制数字,因此这完全是一个幻觉。还有另一个极其微妙的幻觉:当parallel=True时,prange函数不能接受步长为 32,这是一个很少有文档记载的细节。设置parallel=False并进行基准测试,确实比初始提示词工程实现略有改进,比基础实现快 65 倍。
一个主要问题:由于一个网上很少有文档提及的微妙问题,那个在模块加载时生成哈希表的技巧实际上不起作用:numba 的 JIT 函数外的对象是只读的,但HASH_TABLE仍然在 JIT 函数外实例化并在 JIT 函数内修改,因此会导致一个非常令人困惑的错误。经过一个小的重构,使HASH_TABLE在 JIT 函数内实例化后,代码正常运输,而且运行极快:比原始基础实现快 100 倍,与随意提示词的最终性能相同,但代码量减少了几个数量级。
总的来说,要求 LLM 写更好的代码确实能让代码变得更好,这取决于你如何定义更好。通过使用通用的迭代提示词,代码在功能性和执行速度方面都得到了显著提升。提示词工程能更快速且更稳定地改进代码性能,但也更容易引入细微的 bug,这是因为 LLM 本身并非为生成高性能代码而训练的。与使用 LLM 的其他场景一样,效果因人而异。无论 AI 炒作者们如何吹捧 LLM 为神器,最终都需要人工干预来修复那些不可避免的问题。
出乎我意料的是,Claude 3.5 Sonnet 在两个实验中都没有发现和实现某些优化。具体来说,它没有从统计学角度来思考:由于我们是从 1 到 100,000 的范围内均匀生成 1,000,000 个数字,必然会出现大量无需重复分析的数字。LLM 没有通过将数字列表转换为 Pythonset()或使用 numpy 的unique()来去重。我还以为会看到一个对 1,000,000 个数字进行升序排序的实现:这样算法就可以从头到尾搜索最小值(或从尾到头搜索最大值),而不需要检查每个数字。不过排序操作较慢,向量化方法确实更实用。
即使大语言模型可能会出错,我从这些实验中得到的一个重要启示是,即使代码输出不能直接使用,它们仍提供了有趣的想法和工具建议。例如,我从未接触过 numba,因为作为一个数据科学家/机器学习工程师,如果我需要更好的代码性能,我习惯于使用 numpy 的技巧。然而,numba JIT 函数的效果令人难以忽视,我可能会把它加入我的工具箱。当我在其他技术领域(如网站后端和前端)测试类似的“优化代码”提示词迭代工作流时,LLM 也提出了不少有价值的建议。
当然,这些大语言模型不会很快取代软件工程师,因为需要强大的工程师背景以及其他特定领域的知识,才能识别出什么才是真正好的实现。即使互联网上有大量的代码,若没有指导,大语言模型也无法区分普通代码和优秀的高性能代码。现实世界的系统显然比面试式的编程问题复杂得多,但如果通过快速反复要求 Claude 实现一个功能,能使代码速度提高 100 倍,那这个流程就非常值得。有些人认为过早优化[21]是不好的编码实践,但在实际项目中,这比那些随着时间的推移会变成技术债务的次优实现要好得多。
我的实验存在一个局限性,那就是我使用 Python 来对代码改进进行基准测试,而这并不是开发者在追求极致性能优化时的首选编程语言。虽然像 numpy 和 numba 这样的库通过利用 C 语言来解决了 Python 的性能瓶颈,但更现代的解决方案是采用 polars 和 pydantic 等流行 Python 库,它们使用 Rust 开发。Rust 在性能方面比 C 语言更具优势,而 PyO3 几乎没有性能损耗就能让 Python 调用 Rust 代码。我可以确认 Claude 3.5 Sonnet 能够生成兼容 Python 和 Rust 代码,不过这种工作流程太新颖了,足够成为另一篇博文的主题。