利用莱布尼茨公式(Leibniz formula)计算圆周率 $\pi$。尽管在现代数学计算库中,莱布尼茨级数因其收敛速度极慢而鲜被用于实际精算 Π 值,但其算法结构——高密度的浮点运算、紧凑的循环逻辑以及对算术逻辑单元(ALU)的持续压力——使其成为测试 CPU 单核吞吐量、浮点运算单元(FPU)效率以及编译器自动向量化(Auto-vectorization)能力的绝佳“试金石” 。
GitHub 开源项目 niklas-heer/speed-comparison 在 2025 年 12 月产生的最新数据,涵盖了从底层系统级语言(如 C++、Rust)到托管型语言(如 Java、C#),再到动态解释型语言(如 Python、Ruby)的 62 种不同实现。通过对 10 亿次迭代运算的详尽分析,我们不仅试图排列出“谁最快”,更致力于揭示“为什么快”背后的深层技术逻辑,探讨单指令多数据(SIMD)技术、即时编译(JIT)机制以及内存模型对计算性能的决定性影响。
上图来自:https://niklas-heer.github.io/speed-comparison/
莱布尼茨公式,作为 arctan(x) 的泰勒级数在 x=1时的特例,其数学表达为:
从算法实现的角度审视,该公式具有以下显著特征,这些特征直接决定了其作为基准测试的有效性:
- 极端的计算密集度:算法核心仅包含基本的加、减、乘、除运算,几乎不涉及复杂的内存分配或系统调用(System Calls)。这使得测试结果能够高度纯粹地反映语言运行时的计算开销和指令生成质量 。
- 可预测的分支逻辑:公式中的符号交替项 (-1)^k 引入了潜在的分支预测(Branch Prediction)挑战。朴素的实现可能会在循环内部使用 if (i % 2 == 0) 判断奇偶性,这将导致大量的 CPU 分支预测失败,从而严重拖慢流水线。而高效的实现通常采用无分支(Branchless)技巧,利用位运算或数学变换来消除条件跳转,这考验了程序员对底层硬件的理解以及编译器的优化智能 。
- 浮点精度与收敛性:虽然本基准测试明确声明“不关注 pi的精确度”,仅关注运算速度,但浮点数(IEEE 754 标准)的累加特性使得计算顺序对结果有微小影响。编译器是否开启 -ffast-math 等激进优化选项(允许改变浮点结合律)对性能有着数量级的影响,这成为了不同语言实现之间性能差异的主要变量之一 。
基于 2025 年 12 月的最新基准测试数据,我们将 62 种语言实现划分为四个具有显著特征的性能梯队。
第一梯队:极限性能层 (< 300ms) —— 编译器的极致
这一梯队的语言代表了当前通用 CPU 单核计算的物理极限。它们几乎完全消除了语言本身的运行时开销,性能瓶颈仅在于 CPU 的 ALU 吞吐量和内存带宽。
深度剖析:
- LLVM 的霸权:前 10 名中,C++ (Clang)、Zig、D (LDC)、Rust (Nightly) 均依赖 LLVM 编译器后端。这证明了 LLVM 在现代处理器指令调度和向量化分析上的卓越能力。
- C# 的惊人逆袭:C# (SIMD) 位列第二,仅落后 C++ 不到 4 毫秒。这打破了“托管语言一定慢”的刻板印象。通过.NET 的 System.Numerics.Vectors 库,C# 能够生成与 C++ 几乎相同的 AVX-512 机器码,同时享受 JIT 针对当前硬件动态优化的优势 。
- 手写 vs 自动:排名第 4 的 C++ (avx2) 是手写 SIMD 代码,却输给了编译器自动优化的 Clang (第 1)。这说明在简单的循环逻辑中,现代编译器对流水线气泡(Pipeline Bubble)和寄存器分配的掌控已经超越了普通人类专家 。
第二梯队:亚秒级高性能层 (300ms - 1000ms) —— 标量优化的极限
这一梯队的语言性能非常出色,通常在 0.5 秒到 1 秒之间。它们大多生成了高效的机器码,但因未开启激进的 SIMD 优化或受到运行时轻微拖累,未能进入第一梯队。
深度剖析:
- Rust 的版本鸿沟:Rust (Stable) 耗时 633ms,而 Nightly 版仅需 234ms。这种巨大的差距源于 Rust 稳定版对 IEEE 754 浮点行为的严格遵守,阻止了编译器进行改变运算顺序的向量化优化。只有在 Nightly 版中显式启用相关特性,才能释放硬件潜力 。
- Go 的妥协:Go 语言(888ms)稳定地处于这一梯队。Go 的编译器(gc)设计初衷是编译速度快,而非生成的代码最快。它在自动向量化方面远不如 LLVM 激进,且 Go 运行时包含的调度器和垃圾回收(GC)屏障(Write Barriers)在微观层面引入了额外开销 。
- JavaScript 的运行时之战:Bun (928ms) 显著快于 Node.js (1.28s)。Bun 使用的 JavaScriptCore (JSC) 引擎在特定数值计算优化上表现出了相比 Google V8 的优势,证明了现代 JS 引擎的 JIT 能力已能逼近原生代码(仅慢 3-4 倍)。
第三梯队:解释与混合层 (1s - 5s) —— JIT 的战场
这一梯队主要包含动态类型语言的高性能 JIT 实现,或启动开销较大的静态语言环境。
深度剖析:
- PyPy 的惊艳表现:PyPy 将 Python 的运行时间压缩至 1.06 秒,仅比 C# 标准版慢一点。这得益于其 Tracing JIT 技术,能够动态记录循环的执行路径并编译为机器码,消除了动态类型检查的巨大开销 。
- NumPy 的陷阱:虽然 NumPy 底层是 C,但在此测试中(2.46s)表现平平。这是因为测试代码使用了 Python 层的 for 循环逐个调用 NumPy 的标量运算。NumPy 的威力在于数组操作(Vectorization),在标量调用场景下,Python 与 C 之间的上下文切换(Function Call Overhead)反而成为了负担。若允许重写为数组操作,NumPy 可能会进入第一梯队,但这违反了“算法一致性”规则 。
- Java 的启动与优化:标准 Java (1.70s) 表现中规中矩。HotSpot 编译器虽然强大,但在无法自动向量化浮点循环的情况下,受限于 JVM 的栈操作开销。此外,Java 巨大的启动时间(JVM 初始化、类加载)在短时任务中占比显著。
第四梯队:纯解释器层 (> 10s) —— 动态类型的代价
最慢的梯队,主要是未优化的脚本语言解释器。
深度剖析:
- CPython 的性能瓶颈:标准 Python(CPython)以 86.32 秒垫底,比 C++ 慢了近 400 倍。这归因于其虚拟机架构:每一次加法操作都需要进行对象类型检查(Type Checking)、引用计数更新(Reference Counting)和字节码分发(Dispatch)。对于 10 亿次循环,这些微小的开销累积成了巨大的时间鸿沟 。
- 解释器的局限:这一梯队的语言(PHP, Ruby, Perl, Raku)在处理紧凑循环时,CPU 主要忙于解释器自身的逻辑(解析字节码、管理栈),而非执行实际的数学运算。
C#:.NET Core 的高性能复兴
在本次测试中,C# (SIMD) 的表现(227ms)是最令人瞩目的亮点之一。这主要归功于.NET Core(现称为.NET 5/6/7+)引入的硬件内建支持(Hardware Intrinsics)。
- 实现细节:通过引用 System.Runtime.Intrinsics 或使用更高级的 System.Numerics.Vector,C# 开发者可以编写出直接映射到 CPU 向量指令的代码。
- JIT 的优势:与 C++ 的 AOT(提前编译)不同,C# 的 JIT 编译器在程序运行时知道当前 CPU 确切支持哪些指令集(是 AVX2 还是 AVX-512)。这使得 C# 程序可以在旧机器上安全运行,而在新机器上自动全速狂奔,无需像 C++ 那样发布多个二进制版本。基准测试结果证明,这种机制在数值计算领域已经完全成熟 。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |