使用 Python 和 C++ 数值积分计算圆周率 Pi


本文介绍如何利用定积分 tex_76be5892a554905e12ae35ef06fa8411 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 近似计算圆周率 Pi。文章先解释该积分为什么等于 Pi,然后介绍梯形法则和中点法则的数学原理,并给出 Python 与 C++ 的单线程、多线程实现。最后进一步说明如何将积分区间划分到五个分布式计算节点上,并比较不同实现方式的精度、性能、并行能力和适用场景。

integral-math-pi 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

定积分计算PI

圆周率 Pi,通常写作 tex_1a571cd93f36c4348b397731bd2338bc 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 ,广泛出现在数学、物理、工程、统计学和计算机科学中。虽然大多数编程语言都已经提供了内置的 Pi 常量,但亲自计算 Pi 仍然是学习微积分、数值积分、多线程和分布式计算的一个很好的学习的例子。

本文将通过下面这个定积分计算 Pi:

tex_3f534db327af211fd2cc2f0cd10b106a 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

本文将介绍:

  • 为什么这个积分等于 Pi
  • 数值积分如何近似计算定积分
  • 梯形法则
  • 中点法则
  • 单线程 Python 实现
  • 多线程 Python 实现
  • 单线程 C++ 实现
  • 多线程 C++ 实现
  • 如何将同一个算法分配到五个计算节点上

为什么这个积分等于 Pi?

考虑下面的定积分:

tex_2196ac8abfc18f1174202800c855a542 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

关键在于,反正切函数的导数是:

tex_3fd3549da5e94cadd2dfdfbe001cf0f8 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

因此:

tex_2917c1e2b9abf711dba87807b813067e 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

将积分上下限零和一代入:

tex_7ff79e7cca2f916a40b9a1053bcba8b5 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

也就是:

tex_b1041bd6b4631866c3f3e21265241446 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

我们知道:

tex_eb1195772c3c710f3da673e3bfc0a833 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

并且:

tex_aaa6f1caf12477ee4c85133b3216a84f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

因此:

tex_b5e4ffd91441cf60a9317f7ba56e693f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

所以:

tex_7ffc911ef4a982fdb53daf13d6f5a7bb 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

这就为我们提供了一种通过数值积分计算 Pi 的方法。

数值积分是如何工作的?

计算机不一定要通过符号运算直接求出积分的解析解。它可以把积分区间分成许多很小的部分,然后近似计算曲线下面的面积。

假设我们需要计算:

tex_b63d41a1c16f3a1bd136c6773d328857 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

将区间 tex_468127d96274141856229c1626fefa41 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 分成 tex_1feb73aa300ebd9ba23674fb7a10f8ac 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 个长度相等的小区间。

每个小区间的宽度为:

tex_1308967c44e54f32d1c440bb29dfcfe8 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

划分区间的各个点为:

tex_bb8b82b008e5d06ca0008b3af41a1e3c 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

其中:

tex_a99700732065b783efd91a683602fff1 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

近似计算每一个小区域的面积有多种方法。两种常见的方法是:

  • 梯形法则
  • 中点法则

梯形法则

你可能记得的那个包含 tex_1918380ed1434f6e19e6e69ae7fdf8f9 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 tex_e28bdff1200e38e14a3e444d2ece9c7f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 和除以二的公式,就是梯形法则。

对于从 tex_8e0efa9733b95d0da5b2169aa06dcf81 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 tex_b35c00be16b2c40da318b1d92c2b1837 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 的一个小区间,我们使用一条直线连接函数在两个端点上的值。

这样形成的区域是一个梯形。

它的面积近似为:

tex_5cc8102e9e9f4b869083c831368052ec 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

将所有梯形的面积相加,可以得到:

tex_86f756c45b8689c7da8b068f002b3c53 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

一个等价并且通常更容易编程实现的公式是:

tex_64bd5080d50a76d3b2efc119bfe3649c 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

两个端点只计算一半的权重:

tex_5eed05468fe92714cd36ed631785ab65 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

所有内部点使用完整权重:

tex_452f5b2ffe29d56db969ac3be4092d4b 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

对于我们的 Pi 积分:

tex_99bff1a5625c8921b807380a26a87419 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

并且:

tex_5e3ba3c34a3079609603f6781294a710 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

因此:

tex_640a61ca3f8872affde92d620fa909de 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

梯形法则对 Pi 的近似公式为:

tex_d040a712ba5eb2044728db0dae1d79a9 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

因为:

tex_edaadbc8e348403cc0dbfcc6e4e0630d 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

并且:

tex_42d10bbc07bf2053178f359b21960643 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

所以还可以写成:

tex_99c0727f4229d884ea42edb039185ce0 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

一个简单的梯形法则示例

假设我们把积分区间分成四份:

tex_84c2dd7b296eccfd44c62124befd9053 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

每个小区间的宽度为:

tex_9ffd65ec9c5a5dccb5f602a7db195961 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

五个边界点为:

tex_d9da1383e0ed669e5548bf8e4340843e 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

函数在这些点上的值大约为:

tex_970fbd72b9bc6da1272ef939121f9fc0 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

tex_d3455711558c665487cb4b022bf0f877 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

tex_70c4d6eb464cb2105f6ae2543be8fbcb 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

tex_cde6c6f63b3c1ee8e6ceb19dfc4bfd5d 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

tex_d62fc959c95215d9c7089b25aa9a4162 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

应用梯形法则:

tex_9254a5be295fdf1715e73195ca033de2 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

因此:

tex_fc34c1d6e74f9026e55e3f318d13f514 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

Pi 的真实值大约为:

tex_2f49e1b3747ae5d02e51f3db97fe591d 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

即使只划分为四个小区间,我们也已经得到了一个比较合理的近似值。增大 tex_1feb73aa300ebd9ba23674fb7a10f8ac 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 可以进一步提高结果的精度。

中点法则

本文后面的主要代码使用的是中点法则。

梯形法则在每个小区间的边界上计算函数值,而中点法则在每个小区间的中心位置计算函数值。

tex_7ab5725b1f27806587b2bc40f9653e78 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 个区间的中点为:

tex_61450d58c0c6aed9a9572f492f901208 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

该小区间的面积可以近似看成一个矩形:

tex_71302dfa6855d776b48cca433c0051c1 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

将所有矩形面积相加:

tex_8db5070628dd4b3ffc2755c504cf1874 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

对于 Pi 的积分,tex_95c78b93118d9e1aea57ca5413c7d18d 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 tex_3d495795a937f3c66da4466786cd621a 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 ,并且 tex_91288f8c31bc88349c6df84cc3f44145 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

因此:

tex_1368dafc3beabf5da41e95286d70061a 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

并且:

tex_c83b3b0d02595dbfc646276ad8b14d79 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

代入 tex_91288f8c31bc88349c6df84cc3f44145 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

tex_36d61e97e5e3376660bde35d71a99724 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

tex_1feb73aa300ebd9ba23674fb7a10f8ac 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 越大,计算结果通常越接近 Pi 的真实值。

梯形法则与中点法则的比较

方法 函数取值位置 公式
梯形法则 每个区间的边界 tex_0bedc3fd0c8a73488fe99275d1c86661 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机
中点法则 每个区间的中心 tex_9fab18e20c965d97aa7589873c4c5bad 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

对于足够平滑的函数,这两种方法的误差通常都与下面的量成正比:

tex_27692c6b15278ff5b1684732de493c5f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

不过,在使用相同数量小区间的情况下,中点法则通常比梯形法则更加精确。

使用中点法则的单线程 Python 实现

下面的 Python 程序将积分区间划分成 2500 万个小区间。

import math
import time


def calculate_pi(total_steps: int) -> float:
    if total_steps <= 0:
        raise ValueError("total_steps 必须为正数")

    step_width = 1.0 / total_steps
    total = 0.0

    for step in range(total_steps):
        midpoint = (step + 0.5) * step_width
        total += 4.0 / (1.0 + midpoint * midpoint)

    return total * step_width


def main() -> None:
    total_steps = 25_000_000

    started_at = time.perf_counter()
    pi_estimate = calculate_pi(total_steps)
    elapsed_seconds = time.perf_counter() - started_at

    absolute_error = abs(pi_estimate - math.pi)

    print(f"Pi 估算值:    {pi_estimate:.15f}")
    print(f"Pi 参考值:    {math.pi:.15f}")
    print(f"绝对误差:     {absolute_error:.3e}")
    print(f"运行时间:     {elapsed_seconds:.3f} 秒")


if __name__ == "__main__":
    main()

核心计算代码是:

midpoint = (step + 0.5) * step_width
total += 4.0 / (1.0 + midpoint * midpoint)

第一行计算当前小区间的中点:

tex_c8e31a04f7fea4fed738204f83e7817c 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

第二行计算函数值:

tex_c8e61ac299b4d3bb5c5964d2713dbdbe 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

在累加所有函数值之后,再乘以每个小区间的宽度:

return total * step_width

这在数学上对应:

tex_6b7ed9153c0c7d9c64a580cd4b696fef 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

使用梯形法则的单线程 Python 实现

梯形法则版本在各个小区间的边界位置计算函数值,而不是在中点计算。

import math
import time


def calculate_pi_trapezoidal(total_steps: int) -> float:
    if total_steps <= 0:
        raise ValueError("total_steps 必须为正数")

    h = 1.0 / total_steps

    def f(x: float) -> float:
        return 4.0 / (1.0 + x * x)

    total = 0.5 * (f(0.0) + f(1.0))

    for step in range(1, total_steps):
        x = step * h
        total += f(x)

    return total * h


def main() -> None:
    total_steps = 25_000_000

    started_at = time.perf_counter()
    pi_estimate = calculate_pi_trapezoidal(total_steps)
    elapsed_seconds = time.perf_counter() - started_at

    absolute_error = abs(pi_estimate - math.pi)

    print(f"Pi 估算值:    {pi_estimate:.15f}")
    print(f"Pi 参考值:    {math.pi:.15f}")
    print(f"绝对误差:     {absolute_error:.3e}")
    print(f"运行时间:     {elapsed_seconds:.3f} 秒")


if __name__ == "__main__":
    main()

两个端点的贡献通过下面的代码计算:

total = 0.5 * (f(0.0) + f(1.0))

这对应:

tex_9c1dc1092f1a4801e7367643d6bf87e6 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

循环计算所有内部点的函数值之和:

for step in range(1, total_steps):
    x = step * h
    total += f(x)

这对应:

tex_86b25eb4f78ad323a4aa3757c0346605 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

最后再乘以 tex_0150379203a76348438a0deea651610f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

return total * h

使用中点法则的多线程 Python 实现

我们可以把 2500 万个小区间分配给五个工作线程。

对于总共 tex_502d40d526ff202c1fe6b81ca693c2bc 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 个工作线程中的第 tex_0c4924e077c358daddc0041e6e70ab7f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 个工作线程,它的起始位置为:

tex_9a1645e5a1a92fa763931b78d7b9e13e 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

结束位置为:

tex_c2bd0fbe9f9930d55ee7d8bba08a63d7 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

对于五个工作线程和 2500 万个小区间:

工作线程 起始步骤 结束步骤 步骤数量
0 0 5,000,000 5,000,000
1 5,000,000 10,000,000 5,000,000
2 10,000,000 15,000,000 5,000,000
3 15,000,000 20,000,000 5,000,000
4 20,000,000 25,000,000 5,000,000

下面使用 ThreadPoolExecutor 实现:

import math
import time
from concurrent.futures import ThreadPoolExecutor


def calculate_partial_sum(
    start_step: int,
    end_step: int,
    step_width: float,
) -> float:
    partial_sum = 0.0

    for step in range(start_step, end_step):
        midpoint = (step + 0.5) * step_width
        partial_sum += 4.0 / (1.0 + midpoint * midpoint)

    return partial_sum


def calculate_pi_multithreaded(
    total_steps: int,
    worker_count: int,
) -> float:
    if total_steps <= 0:
        raise ValueError("total_steps 必须为正数")

    if worker_count <= 0:
        raise ValueError("worker_count 必须为正数")

    step_width = 1.0 / total_steps
    ranges = []

    for worker_id in range(worker_count):
        start_step = total_steps * worker_id // worker_count
        end_step = total_steps * (worker_id + 1) // worker_count
        ranges.append((start_step, end_step))

    with ThreadPoolExecutor(max_workers=worker_count) as executor:
        futures = [
            executor.submit(
                calculate_partial_sum,
                start_step,
                end_step,
                step_width,
            )
            for start_step, end_step in ranges
        ]

        partial_sums = [
            future.result()
            for future in futures
        ]

    return sum(partial_sums) * step_width


def main() -> None:
    total_steps = 25_000_000
    worker_count = 5

    started_at = time.perf_counter()

    pi_estimate = calculate_pi_multithreaded(
        total_steps,
        worker_count,
    )

    elapsed_seconds = time.perf_counter() - started_at
    absolute_error = abs(pi_estimate - math.pi)

    print(f"工作线程数:   {worker_count}")
    print(f"Pi 估算值:    {pi_estimate:.15f}")
    print(f"Pi 参考值:    {math.pi:.15f}")
    print(f"绝对误差:     {absolute_error:.3e}")
    print(f"运行时间:     {elapsed_seconds:.3f} 秒")


if __name__ == "__main__":
    main()

每个工作线程计算一个部分和:

tex_ff67df356a92c3436575d640ef6b7d0c 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

主线程再把这些结果合并起来:

tex_e6f6e6d801ed6facfa5e084918c36ff9 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

Python 多线程的一个重要限制

这个多线程 Python 版本展示了如何把计算划分为多个独立区间,但它不一定比单线程版本运行得更快。

标准 CPython 存在全局解释器锁,也就是通常所说的 GIL。

对于 CPU 密集型 Python 代码,在同一时刻通常只有一个线程能够执行 Python 字节码。

因此,五个 Python 线程并不意味着能够有效地同时使用五个 CPU 核心。

这个多线程版本可能会出现以下情况:

  • 运行速度与单线程版本基本相同
  • 由于线程管理开销,运行速度反而稍慢
  • 适合演示如何分配计算任务
  • 不适合在普通 CPython 中实现真正的 CPU 并行计算

如果需要在 Python 中实现真正的 CPU 并行,可以考虑:

  • ProcessPoolExecutor
  • multiprocessing 模块
  • NumPy
  • 本地编译扩展
  • MPI
  • 独立的分布式进程

使用中点法则的单线程 C++ 实现

C++ 非常适合执行这种紧密的数值循环,因为它可以被编译为本地机器代码/Native Code。

#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <stdexcept>


double calculate_pi(std::uint64_t total_steps) {
    if (total_steps == 0) {
        throw std::invalid_argument(
            "total_steps 必须为正数"
        );
    }

    const double step_width =
        1.0 / static_cast<double>(total_steps);

    double total = 0.0;

    for (
        std::uint64_t step = 0;
        step < total_steps;
        ++step
    ) {
        const double midpoint =
            (static_cast<double>(step) + 0.5)
            * step_width;

        total += 4.0 / (1.0 + midpoint * midpoint);
    }

    return total * step_width;
}


int main() {
    constexpr std::uint64_t total_steps = 25'000'000;

    const auto started_at =
        std::chrono::steady_clock::now();

    const double pi_estimate =
        calculate_pi(total_steps);

    const auto finished_at =
        std::chrono::steady_clock::now();

    const double elapsed_seconds =
        std::chrono::duration<double>(
            finished_at - started_at
        ).count();

    const double reference_pi = std::acos(-1.0);

    const double absolute_error =
        std::abs(pi_estimate - reference_pi);

    std::cout
        << std::fixed
        << std::setprecision(15);

    std::cout
        << "Pi 估算值:    "
        << pi_estimate
        << '\n';

    std::cout
        << "Pi 参考值:    "
        << reference_pi
        << '\n';

    std::cout
        << std::scientific
        << std::setprecision(3);

    std::cout
        << "绝对误差:     "
        << absolute_error
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(3);

    std::cout
        << "运行时间:     "
        << elapsed_seconds
        << " 秒\n";

    return 0;
}

使用优化选项进行编译:

g++ -O3 -std=c++17 pi_single.cpp -o pi_single

运行程序:

./pi_single

-O3 选项会启用较为激进的编译器优化。如果不启用优化,程序的运行速度可能会慢很多。

使用梯形法则的单线程 C++ 实现

下面的 C++ 程序实现了梯形法则:

tex_cf5deca53a7e3794d3e47ed22e569637 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <stdexcept>


double f(double x) {
    return 4.0 / (1.0 + x * x);
}


double calculate_pi_trapezoidal(
    std::uint64_t total_steps
) {
    if (total_steps == 0) {
        throw std::invalid_argument(
            "total_steps 必须为正数"
        );
    }

    const double h =
        1.0 / static_cast<double>(total_steps);

    double total =
        0.5 * (f(0.0) + f(1.0));

    for (
        std::uint64_t step = 1;
        step < total_steps;
        ++step
    ) {
        const double x =
            static_cast<double>(step) * h;

        total += f(x);
    }

    return total * h;
}


int main() {
    constexpr std::uint64_t total_steps = 25'000'000;

    const auto started_at =
        std::chrono::steady_clock::now();

    const double pi_estimate =
        calculate_pi_trapezoidal(total_steps);

    const auto finished_at =
        std::chrono::steady_clock::now();

    const double elapsed_seconds =
        std::chrono::duration<double>(
            finished_at - started_at
        ).count();

    const double reference_pi = std::acos(-1.0);

    const double absolute_error =
        std::abs(pi_estimate - reference_pi);

    std::cout
        << std::fixed
        << std::setprecision(15);

    std::cout
        << "Pi 估算值:    "
        << pi_estimate
        << '\n';

    std::cout
        << "Pi 参考值:    "
        << reference_pi
        << '\n';

    std::cout
        << std::scientific
        << std::setprecision(3);

    std::cout
        << "绝对误差:     "
        << absolute_error
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(3);

    std::cout
        << "运行时间:     "
        << elapsed_seconds
        << " 秒\n";

    return 0;
}

使用下面的命令编译:

g++ -O3 -std=c++17 pi_trapezoidal.cpp -o pi_trapezoidal

使用中点法则的多线程 C++ 实现

与普通 CPython 线程不同,C++ 线程可以在多个 CPU 核心上同时执行 CPU 密集型计算。

下面的实现将计算任务分配给五个线程。

#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <thread>
#include <vector>


double calculate_partial_sum(
    std::uint64_t start_step,
    std::uint64_t end_step,
    double step_width
) {
    double partial_sum = 0.0;

    for (
        std::uint64_t step = start_step;
        step < end_step;
        ++step
    ) {
        const double midpoint =
            (static_cast<double>(step) + 0.5)
            * step_width;

        partial_sum +=
            4.0 / (1.0 + midpoint * midpoint);
    }

    return partial_sum;
}


double calculate_pi_multithreaded(
    std::uint64_t total_steps,
    std::size_t worker_count
) {
    if (total_steps == 0) {
        throw std::invalid_argument(
            "total_steps 必须为正数"
        );
    }

    if (worker_count == 0) {
        throw std::invalid_argument(
            "worker_count 必须为正数"
        );
    }

    const double step_width =
        1.0 / static_cast<double>(total_steps);

    std::vector<std::thread> workers;

    std::vector<double> partial_sums(
        worker_count,
        0.0
    );

    workers.reserve(worker_count);

    for (
        std::size_t worker_id = 0;
        worker_id < worker_count;
        ++worker_id
    ) {
        const std::uint64_t start_step =
            total_steps
            * worker_id
            / worker_count;

        const std::uint64_t end_step =
            total_steps
            * (worker_id + 1)
            / worker_count;

        workers.emplace_back(
            [
                &,
                worker_id,
                start_step,
                end_step
            ]() {
                partial_sums[worker_id] =
                    calculate_partial_sum(
                        start_step,
                        end_step,
                        step_width
                    );
            }
        );
    }

    for (std::thread& worker : workers) {
        worker.join();
    }

    const double combined_sum =
        std::accumulate(
            partial_sums.begin(),
            partial_sums.end(),
            0.0
        );

    return combined_sum * step_width;
}


int main() {
    constexpr std::uint64_t total_steps = 25'000'000;
    constexpr std::size_t worker_count = 5;

    const auto started_at =
        std::chrono::steady_clock::now();

    const double pi_estimate =
        calculate_pi_multithreaded(
            total_steps,
            worker_count
        );

    const auto finished_at =
        std::chrono::steady_clock::now();

    const double elapsed_seconds =
        std::chrono::duration<double>(
            finished_at - started_at
        ).count();

    const double reference_pi = std::acos(-1.0);

    const double absolute_error =
        std::abs(pi_estimate - reference_pi);

    std::cout
        << "工作线程数:   "
        << worker_count
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(15);

    std::cout
        << "Pi 估算值:    "
        << pi_estimate
        << '\n';

    std::cout
        << "Pi 参考值:    "
        << reference_pi
        << '\n';

    std::cout
        << std::scientific
        << std::setprecision(3);

    std::cout
        << "绝对误差:     "
        << absolute_error
        << '\n';

    std::cout
        << std::fixed
        << std::setprecision(3);

    std::cout
        << "运行时间:     "
        << elapsed_seconds
        << " 秒\n";

    return 0;
}

使用线程支持和优化选项进行编译:

g++ -O3 -std=c++17 -pthread pi_multithreaded.cpp -o pi_multithreaded

运行程序:

./pi_multithreaded

为什么每个线程都使用自己的局部求和变量?

每个线程内部首先使用一个局部变量进行计算:

double partial_sum = 0.0;

线程不会在每一次循环中都修改同一个共享的全局变量。

如果所有线程不断更新同一个共享变量,程序就需要使用互斥锁或者原子操作。

这会引入同步开销和线程竞争,可能抵消多线程带来的性能优势。

因此,程序采用了类似 MapReduce 的模式:

  1. 为每个工作线程分配一段积分区间。
  2. 让每个工作线程独立完成计算。
  3. 每个线程保存一个部分结果。
  4. 等待所有线程结束。
  5. 将所有部分结果相加。

在数学上:

tex_037f2536e9b540830713eccff23a32c4 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

最终近似值为:

tex_406e2ace76c2cb9ba92037a7ac60a9a6 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

将算法扩展到五个分布式计算节点

多线程和分布式计算使用相同的数学划分方法,但它们是两种不同的执行模型。

线程通常具有以下特点:

  • 运行在同一台机器上
  • 共享相同的内存空间
  • 属于同一个进程
  • 通过共享变量进行通信

分布式工作进程通常具有以下特点:

  • 作为独立进程运行
  • 可能运行在不同的物理机器上
  • 不共享普通的进程内存
  • 通过文件、Socket、MPI、RPC 或其他分布式运行时进行通信

在一个五节点任务中,每个进程都会获得一个 Rank:

tex_f3551a3df72e77be2092140ba6e8e0f9 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

进程总数为:

tex_067aafc68622c97ff0cebcc155b663da 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

每个 Rank 使用下面的公式计算自己的任务起始位置:

tex_9a1645e5a1a92fa763931b78d7b9e13e 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

结束位置为:

tex_c2bd0fbe9f9930d55ee7d8bba08a63d7 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

在 Python 中,可以这样分配计算范围:

import os


rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])

start_step = total_steps * rank // world_size
end_step = total_steps * (rank + 1) // world_size

每个 Rank 只计算自己负责的区间:

partial_sum = 0.0

for step in range(start_step, end_step):
    midpoint = (step + 0.5) * step_width

    partial_sum += 4.0 / (
        1.0 + midpoint * midpoint
    )

对于五个 Rank,最终结果为:

tex_e6f6e6d801ed6facfa5e084918c36ff9 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

在前面的 Singularity 示例中,每个 Rank 都会写入一个结果文件:

rank-0.json
rank-1.json
rank-2.json
rank-3.json
rank-4.json

Rank 0 等待全部五个文件生成,然后读取每个文件中的部分和,将它们相加,最终得到 Pi 的估算值。

这个过程实现了三种分布式操作:

  • 屏障同步(Barrier):等待所有工作节点完成计算
  • 收集(Gather):收集所有节点的部分结果
  • 归约(Reduce):将所有部分和相加

基于文件的实现比较简单,也很容易理解,但它假设所有节点都可以访问同一个共享输出目录。

在正式的生产级分布式程序中,通常会使用 MPI 或其他支持集合通信的框架,以获得更加可靠和高效的通信机制。

数值积分方法的精度

对于足够平滑的函数,梯形法则和中点法则的误差通常都与下面的量成正比:

tex_27692c6b15278ff5b1684732de493c5f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

这意味着,当小区间数量加倍时,数值积分误差可能大约缩小为原来的四分之一。

不过,无限增大 tex_1feb73aa300ebd9ba23674fb7a10f8ac 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机 并不意味着可以获得无限精度。

计算机使用有限精度的浮点数存储数据。当程序累加几百万甚至几十亿个数值时,舍入误差也可能逐渐累积。

并行版本和单线程版本在最后几位数字上也可能略有不同,因为浮点数加法并不严格满足结合律:

tex_877f052891680ae910f78df6419a8ae3 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

这些表达式在数学上是等价的,但是在有限精度浮点计算中,改变加法顺序可能会改变最终的舍入结果。

性能方面的注意事项

在以下条件下,多线程 C++ 实现通常会比单线程版本更快:

  • 计算机拥有多个 CPU 核心
  • 计算任务足够大
  • 工作线程数量设置合理
  • 启用了编译器优化

使用五个线程并不保证一定能够获得五倍性能提升。

程序性能还会受到以下因素影响:

  • CPU 核心数量
  • 处理器运行频率
  • CPU 温度以及降频
  • 操作系统的线程调度
  • CPU 缓存行为
  • 线程创建和销毁开销
  • 计算机上正在运行的其他任务

对于 Python,GIL 是主要限制。增加 Python 线程数量通常无法加速纯 Python 的 CPU 密集型循环。

对于分布式计算,节点分配、进程启动、共享存储访问以及结果合并也都会产生额外开销。

使用 2500 万个区间计算 Pi 是一个很好的教学示例,但在真实生产环境中,这个计算任务过于简单,不值得为此分配多个昂贵的 GPU 计算节点。

不同实现方式的比较

实现方式 执行模型 真正的 CPU 并行 预期性能
Python 单线程 一个 Python 线程 实现简单,但速度相对较慢
Python 多线程 多个 Python 线程 通常不能,因为受到 GIL 限制 通常不会比单线程更快
C++ 单线程 一个本地线程 通常比纯 Python 快很多
C++ 多线程 多个本地线程 通常是本地计算中最快的版本
五节点分布式计算 多个独立工作进程 可以扩展,但存在启动和通信开销

总结

下面这个恒等式:

tex_76be5892a554905e12ae35ef06fa8411 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

为我们提供了一个简单的例子,展示如何把微积分问题转换成计算机可以执行的数值计算问题。

梯形法则使用相邻边界点之间的直线近似曲线:

tex_0c15670df687966b00a9b61b8c6b63a3 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

中点法则使用每个小区间的中心位置计算函数值:

tex_11bc33eeb4ddc8a770b60530fc2aebd8 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

同一个数学计算可以通过多种方式执行:

  • 在一个 CPU 核心上运行单个循环
  • 在同一台机器上使用多个线程
  • 使用多个进程和多个 CPU 核心
  • 使用运行在不同计算节点上的分布式工作进程

无论使用哪一种执行模型,背后的数学算法基本不变。

真正发生变化的是:如何划分积分区间、在哪里计算部分和,以及如何将所有部分结果重新合并起来。

这也是很多并行算法背后的核心模式:

任务分割 —> 计算部分结果 —> 合并计算最终结果

tex_16a19b03006e47b3d2ee0ac12dae104f 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

虽然计算 Pi 只是一个简单的示例,但同样的 MapReduce 思想广泛应用于科学模拟、机器学习、数据处理、图形渲染、金融建模和大规模分布式系统中。

数学

英文:Calculating Pi with Numerical Integration in Python and C

  • 2003年高考数学: 一张试卷改变了多少人的命运
    2003年高考数学,因为试卷被盗临时启用备用卷,难度陡增,成为许多考生难以忘记的一场考试。作者回忆当年走出考场后的崩溃与无助,也感慨一张试卷如何改变了无数人的命运。多年以后再回头看,那场考试留下的不只是分数,还有对人生无常的深刻记忆。 2003 年福建高考数学使用的是全国卷。那一年因为四川南充南部县发生高考试卷被盗案,外界普遍说法是临时启用了备用卷,所以数学难度异常高。后来福建从 2004 年开始语文、数学、英语自行命题,直到 2016 年才再次全部科目回归全国卷。 公开报道提到,福建“上一次全部科目使用全国卷是 2003 年”,2004 年起语数英开始自行命题。 发生在四川南充南部县,作案人杨博盗走了语文、英语、文科数学、理科数学、综合等试卷,后来被判刑。 2003全国高考数学卷难度地狱 我是2003年参加高考的。 如果没记错,那时候的考试安排是:第一天上午考语文,下午考数学;第二天上午考英语,下午考理综,也就是物理、化学和生物。 我们当时并不是在本校考试,而是要去另一所学校参加高考。那种感觉现在想起来还很清楚:陌生的考场,紧张的气氛,所有人都绷着一根弦。 但真正让我至今难忘的,是第一天下午的数学。 数学一考完,整个考场外几乎是一片“狼嚎”。很多同学出来以后都崩溃了,听说还有不少人当场哭了。那种难,不是平时考试最后几道大题做不出来的难,而是从选择题开始就让人怀疑人生。 我自己当时也完全懵了。 平时数学正常发挥的话,基本上是120分起跳。可那一次,我在考场里从一开始就觉得不对劲:怎么选择题都这么吃力?怎么大题几乎无从下手?我当时第一反应不是“题太难”,而是怀疑自己是不是今天脑子坏了。…
  • 今年IMC英国数学竞赛很难-我娃搞了个金奖
    我娃就是一普娃。他和我说在之前公校都学不到东西,言下之意就是太简单了。我说那是因为难度还没上去。我在你这年纪(初一)第一次期中考 班级第一,年段第二。黑板上写着前几名的名字表扬。还有当时家长会在操场上坐着,念到我名字表扬上台领了一奖状 还有20元红包。98年20元相当于现在10英镑? 这可能是我人生到现在唯一一次高光时刻吧。当时我妈在台下应该很自豪。 我娃说那你当时应该很用功。我说当时我没咋学。因为太简单了。你现在觉得简单是应为遗传了我的智商,而难度上去就不够了。97努力+3%的天赋。我娃一脸震惊🤯 希望他们多受些挫折 数学这一门学科,会就是会,不会就是不会,所以经常用来看一个娃是不是聪明,是不是有天赋。 这个据说今年特别难,金奖绝对是有实力的。这种水平感觉在perse 也是牛娃了。 关键是娃的爹是MSRC顶尖计算机科学家 你俩的郎才女貌全被遗传走了啊 小时候那几块钱的奖励其实很有意思的,还有小本子发,要是没有那个奖励我估计我还找不到读书的动力 今年的IMC难度大,普遍得分低,他7年级拿Gold非常优秀了。我老大9年级也才拿90来分。 主要是私校这么贵的学费 也没见砸出个什么水花来 不er,你这有点凡尔赛了啊。 去年差2分进入下一轮Olympic。听说Kangroo的分数线很低。 本文一共 492 个汉字,…
  • 组合数学入门(2): 卡特兰数的简介及应用
    组合数学入门(2):卡特兰数 卡特兰数是组合数学中最重要的数列之一。它们出现在许多表面上看起来完全不同的计数问题中,但实际上这些问题都具有相同的内在结构:平衡性、递归性以及“不交叉”约束。 在本文中,我们将介绍卡特兰数/Catalan,展示几个重要公式,并解释一些经典应用场景,特别是路径不能越过对角线的网格行走问题。 什么是卡特兰数? 卡特兰数列如下: 第 n 个卡特兰数的一般公式为: 一个等价形式为: 这两个公式完全等价,在组合数学中都经常出现。 为什么卡特兰数很重要 卡特兰数用于计数许多具有递归结构或平衡结构的问题。它们通常出现在以下情形中: 对象必须以平衡方式构造, 路径必须保持在某个边界之内, 配对之间不能交叉, 结构可以被拆分为独立的左右部分。 因此,它们广泛出现在括号、树、网格、多边形以及栈操作等问题中。 一些重要的卡特兰公式 闭式公式 差分等价形式…
  • 教娃(比教媳妇)编程/数学更有成就感
    从2020年11月22日开始第一课,那时我还在亚马逊 AWS S3 团队,孩子们还很小。最初教娃是每天一课,后来调整为每周三课,再到两课,最后稳定在每周一课(中间还停过大约半年)。 我媳妇说得挺对的——我确实挺喜欢教别人,从中能获得一种满足感和成就感。当然,这只是原因之一。当初教孩子,还有一个小心思:有两个固定“听众”,可以让我更自然地练习表达能力和英语,同时也能顺便刷题,再把知识教给孩子,一举多得。好在两个孩子也挺配合,而且都是理科型,不知道是不是受了我的影响。 其实内容并不局限于编程(数据结构与算法),有时也会穿插一些数据库、数学和逻辑等内容,整体以一种自由探索式的学习为主。 到今天为止,一共给他们上了739节编程课(五年半)。从去年开始,我每天带着弟弟刷 LeetCode,让他多动手实践——他敲代码,我在旁边指导(到今天已经坚持了400天),最近哥哥也加入进来了。说实话,这种陪伴式的成长过程,真的很有成就感。 我以前也试过教我妻子编程,但很快就发现这并不是她感兴趣或擅长的事情。时间一长,她基本也就忘得差不多了。这其实很正常——人往往很难记住那些既不感兴趣、也不常用的知识。这件事也让我意识到:自己擅长,并不代表就一定能教好别人,尤其是在对方缺乏兴趣或动力的情况下。 有人质疑,现在刷题已经没用了。确实,在 AI 时代,单纯为了面试而刷题的意义在下降。但对我来说,教孩子刷题这件事的价值,从来就不只是“做题”本身,而在于它背后的能力培养:比如智力训练、逻辑思维的建立、专注力的提升,以及延迟满足的能力。同时,这也是一种高质量的亲子陪伴方式,在一起解决问题的过程中,关系会变得更紧密。 至于教媳妇,其实意义就完全不一样了。更多是一种尝试去理解彼此思维方式的过程,也是在探索“沟通”和“教学”的边界——你会发现,有些事情不是努力就一定有结果,有些人也不需要被“改变”。与其强行去教,不如尊重差异,找到各自更舒服的相处方式。换句话说,教媳妇最大的收获,反而是让我学会了不再执着于“教会”,而是学会“放手”和“理解”。 RING摄像头有30天(付费)云记录,有时候我会保存一下,以后有空整理一下重温一下,等娃大了,给他们看。 来两张媳妇前几年学编程一脸生无可恋,哈哈。 英文:Programming is not my wife’s…
  • 数学之美: Sigma 函数的推导公式与 Python 实现
    理解 Sigma 函数:因子、乘法性与公式推导 一文看懂 Sigma 函数:因子分解的终极威力! σ(n) 完全解析:为什么求和函数能“自动”变成乘积? 数学之美:Sigma 函数的推导、公式与 Python 实现 从几何级数到质因数:Sigma 函数的魔法公式大揭秘 搞懂 σ(n) 的那一天,我看到了数学的秩序 为什么 σ(n) =…
本文一共 3478 个汉字, 你数一下对不对.
使用 Python 和 C++ 数值积分计算圆周率 Pi. (AMP 移动加速版本)
上一篇: 月光奏鸣曲: 从学校礼堂弹到根特街头
下一篇: 一个人的生命力, 藏在他的欲望里

扫描二维码,分享本文到微信朋友圈
72131?noamp=mobile%2Famp 使用 Python 和 C++ 数值积分计算圆周率 Pi 学习笔记 数学 数学 计算机

评论