
最近在整理一些老项目时翻到了一个经典的汉诺塔递归算法实现。出于好奇我顺手用 JDK8、JDK17、Rust 和 Go 分别跑了一下想看看这个“古老”的算法在不同现代语言环境下的性能表现。本以为结果会大同小异毕竟算法逻辑完全一致但实际跑出来的数据却让我重新思考了一些问题我们常说的“性能”到底在比什么是语言的绝对速度还是运行时环境的优化水平是一次性的基准测试还是长期运行的稳定性这次简单的比对更像是一次“控制变量”的实验。我们剥离了复杂的业务逻辑、网络IO和并发竞争只聚焦于最纯粹的递归计算。结果发现即使在这种看似公平的对比下不同语言和运行时展现出的差异也远不止是“谁快谁慢”那么简单。它背后涉及编译策略、内存模型、运行时优化甚至测试方法本身。如果你也好奇这些差异从何而来以及在实际项目中该如何看待这类“性能测试”那么接下来的内容或许能给你一些不一样的视角。1. 为什么用汉诺塔做性能比对它测的到底是什么在开始看具体数据之前我们得先达成一个共识用汉诺塔递归算法做性能测试到底在测什么这绝不是为了证明“某个语言在计算递归上天下第一”而是为了在一个高度可控、逻辑完全一致的环境下观察不同语言运行时Runtime和编译器Compiler对同一段计算密集型任务的处理方式。汉诺塔问题是一个经典的递归问题其算法时间复杂度为 O(2^n)当盘数n增大时计算步骤会呈指数级增长。这使得它成为一个对函数调用开销、栈帧管理、甚至尾递归优化如果语言支持非常敏感的测试用例。我们写的代码逻辑是完全相同的移动 n 个盘子从 A 柱借助 B 柱移动到 C 柱。这个“相同”保证了我们对比的是语言和运行时的执行效率而不是算法本身的优劣。然而这里有一个关键点容易被忽略我们测试的不仅仅是语言的“执行速度”更是其整个工具链和运行时环境对这类特定计算模式的优化能力。例如函数调用开销每次递归都是一次函数调用。不同语言/运行时创建和销毁栈帧的成本不同。内存访问模式纯粹的递归计算几乎只和栈打交道这能反映出运行时栈管理的效率。编译优化尤其是对于 Rust 和 Go 这类 AOTAhead-Of-Time编译的语言编译器能否对递归进行优化如内联至关重要。而对于 JavaJDK则要看 JITJust-In-Time编译器在运行时能发挥多大作用。基准测试的“公平性”如何预热Warm-up、如何计时、如何避免其他进程干扰这些方法论层面的细节往往比代码本身更能影响结果。所以这次比对的目的不是要得出一个“Rust 最快所以 Rust 最好”的简单结论。而是想通过这个极简的案例拆解性能差异的来源并思考这些差异在更复杂的实际项目中意味着什么。你会看到有时候“快”不一定代表在你的场景里就“好用”。2. 测试环境与代码实现如何确保“控制变量”为了保证对比的可靠性我们必须尽可能排除无关变量的干扰。以下是本次测试的基础设置测试环境硬件同一台笔记本电脑处理器为 Intel Core i7-12700H内存 32GB DDR5。操作系统Windows 11 专业版 22H2。电源模式测试时连接电源并设置为“最佳性能”模式以减少动态频率调整的影响。语言版本与关键配置JDK 8版本1.8.0_401。这是 LTS 版本的一个长期维护更新。JVM 参数使用默认设置未进行特殊调优。JDK 17版本17.0.10。同样是 LTS 版本。JVM 参数也为默认。JDK17 相比 JDK8在 JIT 编译器从 C2 到 C2 的持续优化、垃圾回收器G1 成为默认等方面都有显著变化。Rust版本1.77.2。使用cargo build --release进行编译这将启用最高级别的优化。Go版本1.22.2。使用go build -ldflags-s -w进行编译以减小二进制体积并启用标准优化。核心算法实现逻辑完全一致 以下是 Go 语言的实现示例其他语言的代码在逻辑上与之完全等同。package main import ( fmt time ) func hanoi(n int, from, to, via string) { if n 1 { // 实际测试时这里通常不进行打印输出因为IO会严重干扰计时。 // fmt.Printf(Move disk 1 from %s to %s\n, from, to) return } hanoi(n-1, from, via, to) // fmt.Printf(Move disk %d from %s to %s\n, n, from, to) hanoi(n-1, via, to, from) } func main() { disks : 25 // 测试盘数可根据需要调整 start : time.Now() hanoi(disks, A, C, B) elapsed : time.Since(start) fmt.Printf(Go: Hanoi with %d disks took %v\n, disks, elapsed) }关键注意事项注释掉打印语句在性能测试中控制台 IO (printf) 是极其耗时的操作会完全掩盖计算本身的耗时。因此在计时的核心循环中我们必须将其注释掉只测量纯计算时间。预热Warm-up对于 JavaJVM这类带有 JIT 编译的语言前几次执行速度会慢于后续执行因为 JIT 需要时间将热点代码编译为本地机器码。为了获得稳定结果通常需要在正式计时前先“预热”地执行多次测试函数例如先跑几遍不计时。本次测试对 Java 采用了预热步骤。多次测量取中位数单次运行可能受到操作系统调度、后台进程等偶然因素影响。更可靠的做法是运行多次例如 10 次取中位数或平均值作为最终结果。本次测试采用了多次运行取中位数的方法。选择合适的 n盘数n 太小如 10耗时太短测量误差大。n 太大如 30步骤数超过10亿耗时过长且可能引发栈溢出。需要选择一个能产生明显耗时几百毫秒到几秒且不会出错的 n。本次测试主要使用 n25 和 n26。有了这些严格的控制我们得到的性能数据才更有可比性才能将注意力聚焦在语言和运行时本身的差异上。3. 性能数据对比与现象解读在n25和n26的多次测试下我们得到了一个相对稳定的性能排序耗时从短到长Rust Go JDK17 JDK8具体来说Rust 的 Release 编译版本最快Go 紧随其后但与 Rust 的差距不大JDK17 明显快于 JDK8而 JDK8 是最慢的。这个结果本身可能并不出乎很多人的意料但数字背后的原因值得深究。3.1 Rust 为何领先AOT 编译与零成本抽象Rust 的性能优势主要源于其设计哲学和编译模型AOT 编译与极致优化cargo build --release开启了包括内联、循环优化、死代码消除等在内的所有优化器通道。对于hanoi这种小函数编译器很容易将其内联展开极大地减少了函数调用的开销。最终生成的是高度优化的本地机器码。零运行时开销Rust 没有垃圾回收GC机制在纯计算场景下避免了 GC 可能带来的暂停。同时其所有权系统在编译期就确定了内存的生命周期运行时无需额外的引用计数或垃圾扫描。栈分配优先递归调用中的参数和局部变量都存储在栈上Rust 对栈内存的访问和管理非常高效。简单来说Rust 编译器在编译阶段就尽可能多地把工作做完了交给 CPU 执行的就是一套为这个特定算法量身定制的、极其高效的指令序列。3.2 Go 的表现简洁与高效的平衡Go 语言同样采用 AOT 编译生成静态二进制文件。它的性能接近 Rust体现了其运行时和编译器的效率。高效的 Goroutine 栈虽然本次测试未使用并发但 Go 的栈管理设计分段栈/连续栈本身追求高效。函数调用开销被控制得很好。编译优化Go 的编译器也会进行内联等优化。虽然其优化激进程度可能不及 Rust 的 LLVM 后端但对于此类算法也已足够。更少的抽象惩罚Go 的语言设计相对简洁运行时比 JVM 轻量得多没有复杂的字节码解释和 JIT 编译阶段从启动到执行峰值速度的路径更短。Go 在“开发效率”和“执行效率”之间取得了很好的平衡在这个测试中得到了印证。3.3 JDK17 对 JDK8 的超越JVM 的进化JDK17 比 JDK8 快这个差距是显著的。这主要归功于多年来 JVM 特别是 JIT 编译器C2的持续优化。更智能的 JIT 编译JDK17 的 C2 编译器包含了更多优化算法能更好地分析热点代码进行内联、逃逸分析、锁消除等。对于hanoi这种紧凑的递归循环JIT 能将其编译成质量很高的本地代码。预热优势在充分预热后JDK17 的峰值性能可以非常接近本地编译语言。我们的测试包含了预热步骤因此测量到的是其“稳定态”性能。垃圾回收器改进虽然本次纯计算测试几乎不产生堆内存分配所有操作在栈上但 G1 作为 JDK9 以后的默认 GC其整体设计和效率提升也是 JVM 性能进步的一部分。这告诉我们即使使用同一门语言Java升级运行时环境也可能带来免费的午餐。对于长期运行的服务端应用JDK17 等新版本 LTS 带来的性能提升是实实在在的。3.4 JDK8 为何垫底时代的局限JDK8 是一个里程碑式的版本但它的 JIT 编译器C2与现代版本相比确实存在差距。其优化能力相对较弱对于深度递归这类代码的编译可能不够激进。它代表了多年前的优化技术水平。4. 从微观测试到宏观选择性能数据的实际意义看到这里你可能会想“所以项目里无脑选 Rust 就对了” 且慢。这次测试是一个高度简化的微观模型它揭示了底层执行的效率差异但绝不能直接映射到复杂项目的技术选型。我们需要建立更立体的认知框架。4.1 理解性能的多个维度性能不是一个单点指标而是一个多维度的集合计算性能本次测试焦点纯 CPU 执行指令的速度。对算法交易、科学计算、游戏引擎等场景至关重要。并发与并行性能利用多核的能力。Go 的 Goroutine 和 ChannelRust 的 Fearless ConcurrencyJava 的并发工具包各有千秋测试方式完全不同。内存效率包括内存占用和分配/回收速度。Rust 无 GC控制最精细Go 的 GC 追求低延迟Java 的 GC 可调优但相对复杂。启动性能与预热时间Go 和 Rust 是原生二进制启动即达峰值。Java 需要 JVM 启动和 JIT 预热对于短生命周期的命令行工具或 Serverless 函数这可能是个问题。生态与开发效率这是最重要的“性能”之一。一个拥有丰富库、成熟框架、强大工具链和充足人才储备的生态能极大提升项目的整体交付速度和稳定性。Java 和 Go 在企业级后台服务领域的生态优势巨大。4.2 技术选型决策矩阵我们可以用一个简单的决策框架来思考而不是只看一个维度的跑分考量维度RustGoJava (JDK17)说明极致计算性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐对底层控制、零开销抽象要求极高的场景。高并发网络服务⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Go 的并发模型简单高效Rust 安全但心智负担稍高Java 生态成熟。快速开发与迭代⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Go 语法简单编译快Java 框架丰富Rust 学习曲线陡峭。已有团队技术栈---这是最重要的因素之一。迁移成本可能远超语言本身的性能收益。云原生与微服务⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Go 在 Docker/K8s 生态中占优Java 有 Spring CloudRust 在快速崛起。系统编程与嵌入式⭐⭐⭐⭐⭐⭐⭐⭐Rust 和 C/C 竞争Go 因 GC 和运行时受限Java 基本不适用。安全性要求极高⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Rust 的所有权模型在编译期消除大量内存安全漏洞。4.3 给开发者的实操建议基于以上分析我们可以得出一些更落地的建议不要用单一基准测试决定技术栈汉诺塔测试只是一个切入点。你应该为你项目中最关键的操作如 JSON 序列化、数据库查询、模板渲染、特定算法设计基准测试Benchmark。优先考虑团队与生态对于一个需要快速上线的业务系统选择团队最熟悉、社区支持最力的语言很可能是 Java 或 Go其带来的开发速度和稳定性收益远大于用 Rust 重写可能带来的那点性能提升。在瓶颈处寻求优化而非全局重写如果你的 Java 服务性能遇到瓶颈首先应该使用 Profiler如 Async Profiler找到热点。很可能问题在于某段低效的 SQL、不合理的缓存策略或算法复杂度而不是语言本身。优化热点代码或者用 JNI 调用本地库如用 Rust/C 重写核心计算模块往往是性价比更高的方案。将性能测试纳入开发流程对于关键路径的代码变更可以引入简单的基准测试作为 CI/CD 的一部分防止性能回退。理解不同语言的适用场景Rust适合对性能、内存安全、并发控制有极致要求的场景如操作系统、浏览器引擎、游戏引擎、区块链核心、高性能中间件代理、数据库。Go适合需要快速开发高并发、分布式网络服务的场景如 API 网关、微服务、云计算工具、DevOps 平台、命令行工具。Java (JDK17)适合大型、复杂、需要长期维护的企业级应用特别是已有庞大 Java 生态和团队积累的领域如金融核心系统、电商后台、大数据处理框架Hadoop/Spark。回到最初的汉诺塔测试它的价值不在于告诉我们谁赢了而在于它像一个棱镜折射出不同语言设计哲学和运行时技术的差异。Rust 展示了编译期极致优化的力量Go 体现了简单设计与高效执行的平衡而 JDK17 相对于 JDK8 的进步则提醒我们持续更新基础工具链的重要性。在实际工作中放下“语言之争”的偏见根据项目需求、团队能力和长期维护成本做出务实的选择才是真正的“高性能”开发之道。性能终究是服务于业务目标和工程效率的而不是一个孤立存在的数字游戏。