并发性能提升 200%+ !Python no-GIL 实验版本性能实测

619 阅读4分钟

1. GIL 机制解析

1.1. 什么是 GIL?

全局解释器锁(Global Interpreter Lock,简称 GIL)是 CPython 解释器的核心同步机制,其本质是:

  • 单进程级别的互斥锁。
  • 控制 Python 字节码执行的准入机制。
  • 确保解释器内部状态的线程安全。

1.2. GIL 的运行时瓶颈

1)每个线程执行计算任务前,都需要获取 GIL。

2)进入网络 IO 等待时释放 GIL,其他线程抢占后继续执行计算任务。

3)多线程执行需频繁获取/释放 GIL,使得 Python 并发得到近似单线程的效果。

sequenceDiagram
    participant Web
    participant Thread1
    participant GIL
    participant Thread2

    activate Thread1 #FFA500
    Thread1 ->> GIL : acquire
    Thread1 ->> Thread1: run
    GIL ->> Thread1 : release
    deactivate Thread1
    
    activate Thread1
    Thread1 ->> Web : request(I/O)
    Web ->> Thread1 : response
    deactivate Thread1
    
    %% Thread2 竞争阶段
    loop 计算密集型任务
        activate Thread2
        Thread2 ->> GIL: acquire
        Thread2 ->> Thread2: run
        Thread2 ->> GIL: release
        deactivate Thread2
    end

    activate Thread1

    activate Thread1 #FFA500
    Thread1 ->> GIL : acquire
    Thread1 ->> Thread1: run
    GIL ->> Thread1 : release
    deactivate Thread1

1.3. no-GIL

📢 PEP703 提到在 CPython 中全局解释器锁(GIL)将成为可选项目(Making the Global Interpreter Lock Optional in CPython):

  • a. Short term, we add the no-GIL build as an experimental build mode, presumably in 3.13 (if it slips to 3.14, that is not a problem).

    短期内,将 no-GIL 构建添加为实验性构建模式。

  • b. Mid-term, after we have confidence that there is enough community support to make production use of no-GIL viable, we make the no-GIL build supported but not the default (yet).

    中期,在我们确信有足够的社区支持使无 GIL 的生产使用变得可行之后,我们将支持无 GIL 构建,但暂时不将其作为默认构建。

  • c. Long-term, we want no-GIL to be the default, and to remove any vestiges of the GIL (without unnecessarily breaking backward compatibility).

    长期来看,我们希望 no-GIL 成为默认方式,并删除 GIL 的所有痕迹。

🎉 好消息是,3.13t-dev 作为 no-GIL 实验版本已具备较强的可测试性,下文将其性能进行测试和对比。

2. 安装实验版本

1)通过 pyenv 安装实验版本:

$ pyenv install 3.13t-dev

2)验证已关闭 GIL:

python -c 'import sys; print(sys._is_gil_enabled())'

3. 性能实测

通过构造一个 Redis IO 密集型操作的场景,对比 Python 3.8.12 & 3.13t-dev 在串行 / 并行上的性能表现。

3.1. 对比基准

1)开发环境:MacBook M1 Pro 32 GB。

2)运行一个本地 Redis,版本:7.0.12

$ redis-server&

3)获取本地 Redis SET 命令性能基准:53,561 requests/sec

$ redis-benchmark -t set -n 10000

# Summary:
#  throughput summary: 53561.86 requests per second
#  latency summary (msec):
#          avg       min       p50       p95       p99       max
#        0.505     0.176     0.511     0.679     1.207     2.751

3.2. 编写 Python benchmark

1)安装 throttled-py,该库提供 Redis 及 Benchmark 依赖:

$ pip install throttled-py

2)编写一个压测脚本:

  • 场景(Redis):SET key value
  • 串行:顺序执行 100,000 次。
  • 多线程并发:16 工作线程,执行 100,000 次。
import sys
import redis
from throttled import utils

url: str = "redis://127.0.0.1:6379/0"
client: redis.Redis = redis.Redis(connection_pool=redis.ConnectionPool.from_url(url=url))


def redis_baseline():
    client.set("ping:baseline", 1)


def main():
    try:
        print(f"version: {sys.version} \nis_gil_enabled: {sys._is_gil_enabled()}")
    except AttributeError:
        print(f"version: {sys.version} \nis_gil_enabled: True")

    benchmark: utils.Benchmark = utils.Benchmark()
    # 串行测试(单线程顺序执行)
    benchmark.serial(redis_baseline, 100_000)
    # 并发测试(16 工作线程)
    benchmark.current(redis_baseline, 100_000, workers=16)


if __name__ == "__main__":
    main()

3.3. 性能对比

3.3.1. QPS

在不同 Python 版本间,运行 3.2 的 Benchmark 程序,记录 QPS。

Python 版本串行 x 100,000线程池(16 workers)x 100,000
[Redis SET 基准]53,561 requests/sec53,561 requests/sec
Python 3.8.1218,640 requests/sec13,069 requests/sec
Python 3.13t-dev17,609 requests/sec🚀 39,494 requests /sec

对比得出:

  • Python 3.8.12 串行甚至比多线程并发更快。
  • Python 3.13t-dev 较 Python 3.8 多线程提升 200%+

3.3.2. CPU 使用率

1)Python 3.8.12:🐌 只能跑满一个核心(假多线程实锤)。

2)Python 3.13t-dev:🔥跑满 6 个核。

4. 扩展 Benchmarks 分析

4.1. Pyperformance / 单线程

4.1.1. 什么是 Pyperformance?

Pyperformance 是 Python 的官方基准测试套件,用于测量和比较不同版本 Python 解释器的单线程运行性能。

4.1.2. 运行 Benchmarks

1)运行 Benchmarks

# no-GIL
$ pyperformance run -b nbody,regex_v8,crypto_pyaes,json_dumps,logging -o no_gil_results.json --python=python3.13t-dev 
# with-GIL
$ pyperformance run -b nbody,regex_v8,crypto_pyaes,json_dumps,logging -o with_gil_results.json --python=python3.13.1
# Compare
$ pyperformance compare with_gil_results.json no_gil_results.json

2)性能对比

基准测试 [1]Python 3.13.1(with-GIL)[2]🐌 Python 3.13t-dev(no-GIL)[3]对比 [4]
crypto_pyaes58.8 ms85.1 ms⬇️ 44.7 %
json_dumps7.96 ms10.05 ms⬇️ 26.3 %
logging_format4.25 μs8.86 μs⬇️ 108.5 %
logging_silent75.6 ns156.0 ns⬇️ 106.3 %
logging_simple3.85 μs8.00 μs⬇️ 108.8 %
nbody70.8 ms192.5 ms⬇️ 171.9 %
regex_v819.4 ms20.3 ms⬇️ 4.6 %
  • [1] 在 Pyperformance 中选取计算密集型的 benchmarks,用于反映单线程执行性能。
  • [2] Python version: 3.13.1 (64-bit), Report on macOS-14.7.1-arm64-arm-64bit-Mach-O, Number of logical CPUs: 10。
  • [3] Python version: 3.13.2+ (64-bit) revision 646b453, Report on macOS-14.7.1-arm64-arm-64bit-Mach-O, Number of logical CPUs: 10。
  • [4] no-GIL 实验版本在单线程场景下性能显著下降,可能为保证线程安全,引入额外开销。

4.2. 多线程场景

构造计算密集、IO 密集型原子任务,在 8-threads 模式下进行性能分析,详见 no-gil/benchmarks/main.py

基准测试 [1]Python 3.13.1(with-GIL)Python 3.13t-dev(no-GIL)对比 [2]
is_prime2,493 requests/sec9,768 requests/sec⬆️ 292% [2]
fibonacci462 requests/sec215 requests/sec⬇️ 53.5% [2]
matrix_multiply108 requests/sec103 requests/sec➖ 持平 [3]
redis_set15,923 requests/sec38,020 requests/sec⬆️ 139% [4]
  • [1] is_prime、fibonacci、matrix_multiply 为计算密集型任务,redis_set 为 IO 密集型任务。
    • is_prime:求解 2 ^ 29 - 3 是否为素数。
    • fibonacci:生成长度为 n 的斐波那契数列。
    • matrix_multiply:n 阶矩阵乘法(numpy)。
    • redis_set:执行 SET KEY VALUE
  • [2] no-GIL 在多线程处理计算密集型任务(is_prime)上具有较好的性能表现,涉及申请大量内存(fibonacci)时性能表现不佳。
  • [3] numpy 底层为 C 实现,性能持平。
  • [4] IO 密集型场景下,性能显著提升。

5. 结语

  • GIL 的存在使得过往部分线程不安全的代码得以正常运行,这可能会是未来升级 no-GIL 的隐患。
  • no-GIL 在 IO 密集型任务上具有较好的性能表现,但在需要频繁申请内存的场景下性能表现不佳,具有较大优化空间。
  • no-GIL 带来的性能提升,为 Python 在机器学习、大数据处理等场景下,提供了更多可能性。