)
Python 性能优化实战指南从 GIL 与 PyPy 到 Cython 和并发加速python-guide Speed 场景全解【免费下载链接】python-guidePython best practices guidebook, written for humans.项目地址: https://gitcode.com/gh_mirrors/py/python-guide本文基于《Hitchhikers Guide to Python》本仓库python-guide的 Speed 场景章节 展开。该章节是这本写给人类看的 Python 最佳实践手册中专门回答Python 变慢怎么办的一章CPython 对 CPU 密集型任务偏慢如何用替代解释器、C 扩展与并发手段提速。读完本文你将掌握一套可立即上手的加速决策框架——用 PyPy 换解释器、用 Cython 编译热点函数、用concurrent.futures和threading正确处理并发并理解 GIL 在这条决策链中的核心地位。先看数据CPython 为什么慢PyPy 为什么快文档开篇即点明主题CPythonPython 最常用的实现在处理 CPU 密集型任务时偏慢而 PyPy 快。这里实现implementation一词很关键——正如 which-python.rst 所强调的Python本质是一门语言规范可以被多种方式实现CPython 是 C 语言编写、先把源码编译为字节码再由虚拟机解释执行的参考实现PyPy 则是用 RPythonPython 的一个受限静态类型子集编写的解释器内置即时编译器JIT目标是在保持与 CPython 最大兼容性的同时提升性能。同一份 Python 代码换一个解释器就能获得数量级的性能差异。文档采用了一个略作修改的 David Beazley CPU 密集型基准测试代码在原版基础上加了循环以便多次测量在同一台机器上分别运行 PyPy 与 CPython得到如下对比原文测量环境为 2011 年的 Python 2.7.1 与 PyPy 1.7.0数值用于演示数量级差异不同硬件与版本下具体数字会变但相对量级具有参考意义# PyPy $ ./pypy -V Python 2.7.1 (7773f8fc4223, Nov 18 2011, 18:47:10) [PyPy 1.7.0 with GCC 4.4.3] $ ./pypy measure2.py 0.0683999061584 0.0483210086823 0.0388588905334 0.0440690517426 0.0695300102234# CPython $ ./python -V Python 2.7.1 $ ./python measure2.py 1.06774401665 1.45412397385 1.51485204697 1.54693889618 1.60109114647同样的五次循环PyPy 稳定在 0.040.07 秒CPython 则从 1.07 秒一路升到 1.60 秒——差距超过一个数量级。这正是 PyPy 的 JIT 编译器对纯计算型循环的典型增益。本仓库的 which-python.rst 也佐证在基准测试集上PyPy 目前比 CPython 快数倍如果你的目标是提升既有 Python 代码的性能值得先给 PyPy 一次机会。需要说明的适用前提PyPy 的加速红利主要体现在 CPU 密集的纯 Python 代码上依赖 C 扩展如部分科学计算栈的项目CPython 仍是唯一选项详见后文写 C 扩展时的线程注册与 clibs.rst。性能问题的根源GIL全局解释器锁要理解为什么换解释器、为什么用多进程这些方案必须先理解 GIL。文档给出了它的准确定义GILGlobal Interpreter Lock全局解释器锁是 Python 允许多线程同时运作的机制。Python 的内存管理并非完全线程安全因此需要 GIL 防止多个线程同时执行同一段 Python 代码。换句话说在任意时刻一个 CPython 进程内只有一个线程能执行 Python 字节码——多线程被并发调度交错执行而不是真正并行。这对两类任务的影响截然不同CPU 密集型任务多个线程争抢同一个 GIL无法利用多核并行性能不仅不提升还可能因锁竞争而下降I/O 密集型任务如网络请求、文件读写线程在等待 I/O 时会释放 GIL其他线程得以运行所以多线程依然有效。文档特别推荐了 David Beazley 对 GIL 运作机制的深入研究含 Python 3.2 新版 GIL 的分析并给出结论想要最大化 Python 应用性能必须深刻理解 GIL、它对你具体应用的影响方式、你的机器有几个核以及应用的瓶颈在哪里。这四者共同决定你应该选哪条加速路线——是换解释器、写 C 扩展还是用进程绕开 GIL。写 C 扩展时的线程注册如果你决定用 C 扩展包括 Cython 生成的原生模块来加速文档强调了一条容易被忽略的注意事项编写 C 扩展时必须特别小心确保把线程正确注册到解释器参见 Python C-API 中关于线程初始化的部分。原因正是 GIL 的机制C 扩展中的代码可以主动释放 GIL 以允许其他 Python 线程执行但如果扩展没有正确注册线程、没有在适当位置释放/重新获取 GIL就可能导致死锁或与其他线程互相阻塞。这条经验也解释了为什么换个解释器PyPy与写 C 扩展两条路线在某些场景下是互斥的——C 扩展通常深度耦合 CPython 的 C-API。用 Cython 编译热点代码Python 语法 C 类型Cython 是什么文档将Cython定位为 C 扩展路线的主角它实现了 Python 语言的超集可以用它编写 Python 的 C/C 模块也可以直接调用已编译 C 库中的函数。Cython 的核心价值在于允许你利用 Python 的强类型特性——通过给变量标注 C 类型让编译器生成高效的 C 代码从而把纯 Python 解释执行的循环变成原生机器码。强类型化示例素数计算Cython vs 纯 Python文档给出了同一算法的两份实现第一份是带 Cython 关键字的版本def primes(int kmax): Calculation of prime numbers with additional Cython keywords cdef int n, k, i cdef int p[1000] result [] if kmax 1000: kmax 1000 k 0 n 2 while k kmax: i 0 while i k and n % p[i] ! 0: i i 1 if i k: p[k] n k k 1 result.append(n) n n 1 return result第二份是标准 Python 语法、无任何类型声明的版本def primes(kmax): Calculation of prime numbers in standard Python syntax p range(1000) result [] if kmax 1000: kmax 1000 k 0 n 2 while k kmax: i 0 while i k and n % p[i] ! 0: i i 1 if i k: p[k] n k k 1 result.append(n) n n 1 return result差异剖析多出来的声明做了什么对比两份实现的开头部分就能看清 Cython 的加速原理def primes(int kmax): Calculation of prime numbers with additional Cython keywords cdef int n, k, i cdef int p[1000] result []def primes(kmax): Calculation of prime numbers in standard Python syntax p range(1000) result []差异集中在三处def primes(int kmax)函数参数声明为 C 的intcdef int n, k, i局部变量声明为 C 整数类似标准 C 的写法如第 3 行的cdef int n,k,i循环计数器不再走 Python 对象的装箱/拆箱cdef int p[1000]声明一块 C 语言的固定大小整数数组替代 Python 的range(1000)列表。这些额外的类型声明让 Cython 编译器能够从同一算法生成远为高效的 C 代码循环中的算术、取模、数组索引全部变成原生 C 操作不再涉及 Python 对象的动态分派。文件组织上标准 Python 代码存为*.py文件Cython 代码则存为*.pyx文件。pyximport一行代码编译并导入 .pyxCython 代码怎么用起来文档提供了一个极简的免构建方案——pyximportimport time # Activate pyx compiler import pyximport pyximport.install() import primesCy # primes implemented with Cython import primes # primes implemented with Python print(Cython:) t1 time.time() print(primesCy.primes(500)) t2 time.time() print(Cython time: %s % (t2 - t1)) print() print(Python) t1 time.time() print(primes.primes(500)) t2 time.time() print(Python time: %s % (t2 - t1))其中这两行是点睛之笔import pyximport pyximport.install()pyximport模块允许你直接 import*.pyx文件例如primesCy.pyx内含 Cython 编译版的primes函数。pyximport.install()的作用是让 Python 解释器在 import 时自动启动 Cython 编译器把.pyx源码先生成 C 代码再自动编译为.so动态库最后由 Cython 帮你在 Python 代码中轻松高效地导入这个库。整个编译 → 链接 → 导入链路对开发者透明无需手工维护setup.py构建脚本——非常适合原型验证与热点函数加速。实测性能对比文档在两类硬件上实测了找出 500 个素数所需时间标准笔记本双核 AMD E-450 1.6 GHzCython time: 0.0054 seconds Python time: 0.0566 seconds嵌入式 ARM BeagleBone 开发板Cython time: 0.0196 seconds Python time: 0.3302 seconds在笔记本上 Cython 快约 10 倍在嵌入式 ARM 上快约 17 倍——再次印证对纯 Python 的 CPU 密集循环类型标注 编译带来的加速是数量级的。实际项目中可以把这套模式推广为性能分析定位热点函数 → 用 Cython 强类型化重写 → pyximport 或常规构建集成而 Cython 官网与 Python 社区关于*.pyx编译的文档可作为进一步学习的入口。文档中列出的其他 C 扩展方向原文档在 Cython 之后还列出了Pyrex与Shedskin?两个小节标题但未展开正文。可以确认的信息是Pyrex 是 Cython 的前身方言Cython 即由 Pyrex 演进而来两者在cdef类型声明的思路上一脉相承Shedskin 则属于实验性质的编译器方向。读者在选型时以文档详述的 Cython 为主线即可此外本仓库的 clibs.rst 还覆盖了另一条 C 加速路线——用 ctypes、CFFI、SWIG 等工具直接对接已有 C/C 库适合热点不在 Python 代码里、而在底层库里的场景。并发与并行concurrent.futures加速的第二条主线是并发。文档介绍的标准库concurrent.futures模块提供用于异步执行可调用对象的高层接口把多线程/多进程的复杂细节抽象掉让你专注任务本身。它暴露两个核心类类工作机制适用场景按 GIL 推理ThreadPoolExecutor创建一组工作线程任务提交后在空闲工作线程中执行任务包含大量阻塞如发起网络请求ProcessPoolExecutor同样地提交任务但用多个进程作为 worker任务计算密集可绕开 GILProcessPoolExecutor之所以能绕开 GIL是因为每个进程都有独立的解释器与独立的内存空间但也正因任务要通过 IPC 传给 worker 进程只有可 pickle序列化的对象才能被传入和返回——这是进程池方案必须记住的约束。规则阻塞用线程池计算用进程池文档给出了简洁的黄金法则任务大量阻塞例如发起网络请求→ 用ThreadPoolExecutor任务计算密集 → 用ProcessPoolExecutor。背后正是 GIL 的行为差异阻塞期间线程让出 GIL线程池依然高效纯计算则会互相争锁必须靠多进程实现真正的并行。并行执行两种姿势map 与 submitmap(func, iterables)方法几乎等价于内建map()区别在于它会并行执行所有任务from concurrent.futures import ThreadPoolExecutor import requests def get_webpage(url): page requests.get(url) return page pool ThreadPoolExecutor(max_workers5) my_urls [http://google.com/]*10 # Create a list of urls for page in pool.map(get_webpage, my_urls): # Do something with the result print(page.text)若需要更细粒度的控制用submit(func, *args, **kwargs)它把可调用对象调度为func(*args, **kwargs)执行并立即返回一个表示该调用执行状态的Future对象。Future 对象方法速查表文档完整给出了 Future 的核心方法引用自官方concurrent.futures.Future语义整理如下方法行为cancel()尝试取消该调用cancelled()调用被成功取消时返回Truerunning()调用正在执行且无法取消时返回Truedone()调用成功取消或已运行结束时返回Trueresult()返回调用返回值注意默认会阻塞直到被调度的可调用对象返回exception()返回调用抛出的异常未抛异常时返回None同样会像result()一样阻塞add_done_callback(fn)挂一个回调函数在被调度的可调用对象返回时以fn(future)方式执行submit配合 Future 的完整示例判断大数是否为素数文档将as_completed与ProcessPoolExecutor组合使用为使代码可直接运行这里对原示例做了规范化修正——as_completed迭代的是 future 而非其结果需先调用future.result()from concurrent.futures import ProcessPoolExecutor, as_completed def is_prime(n): if n % 2 0: return n, False sqrt_n int(n**0.5) for i in range(3, sqrt_n 1, 2): if n % i 0: return n, False return n, True PRIMES [ 112272535095293, 112582705942171, 112272535095293, 115280095190773, 115797848077099, 1099726899285419] futures [] with ProcessPoolExecutor(max_workers4) as pool: # Schedule the ProcessPoolExecutor to check if a number is prime # and add the returned Future to our list of futures for p in PRIMES: fut pool.submit(is_prime, p) futures.append(fut) # As the jobs are completed, print out the results for future in as_completed(futures): number, result future.result() if result: print({} is prime.format(number)) else: print({} is not prime.format(number))注意with ProcessPoolExecutor(max_workers4) as pool:的用法Executor 支持上下文管理器协议退出with块时会自动调用shutdown()等待所有任务结束并释放资源。两个辅助函数as_completed 与 wait模块还提供两个操作 Future 集合的辅助函数as_completed(futures)返回一个迭代器按完成顺序逐个产出已完成的 future——上面的素数示例就是用它边完成边打印结果wait(futures)阻塞直到传入列表中的所有future 全部完成。map/submit负责发起并行as_completed/wait负责汇聚结果这四件套覆盖了绝大多数批量并行任务的需求。手工线程threading 模块当需要更底层的控制时标准库还提供threading模块让你手动管理多个线程。启动一个线程Thread start把可调用对象及其参数传给Thread构造器再调用start()就能在另一个线程中运行它from threading import Thread import requests def get_webpage(url): page requests.get(url) return page some_thread Thread(get_webpage, http://google.com/) some_thread.start()注文档示例保留了Thread(get_webpage, url)的位置参数写法。按threading.Thread的规范签名更稳妥的写法是Thread(targetget_webpage, args(http://google.com/,))显式指定target与args避免误把位置参数当成group等参数。等待线程结束join 与 is_alive调用join()会阻塞当前线程直到该线程终止some_thread.join()join()支持可选的超时参数。文档建议调用join()之后最好检查线程是否还活着因为 join 可能已超时返回if some_thread.is_alive(): print(join() must have timed out.) else: print(Our thread has terminated.)数据竞争与 Lock 保护因为多个线程共享同一片内存可能出现两个或更多线程同时写同一资源、或输出依赖事件先后顺序的情况——这就是数据竞争race condition输出会错乱或出现极难排查的问题。文档引用的经典案例是多个线程同时往终端打印导致文本互相穿插。规避手段是用Lock每个线程在写共享资源前必须先获取锁。锁既可通过上下文管理器协议with语句获取/释放也可直接用acquire()与release()。文档给出了一个略显刻意但清晰的示例——多个线程监控一组网站发现变更时写日志文件from threading import Lock, Thread file_lock Lock() def log(msg): with file_lock: open(website_changes.log, w) as f: f.write(changes) def monitor_website(some_website): Monitor a website and then if there are any changes, log them to disk. while True: changes check_for_changes(some_website) if changes: log(changes) websites [http://google.com/, ... ] for website in websites: t Thread(monitor_website, website) t.start()这里每个线程在写入文件前都会执行with file_lock:等待获取锁从而保证任意时刻只有一个线程在写文件。with语句还自动处理了即使抛异常也释放锁的语义比手动acquire()/release()更不易出错。请注意示例中的log()以w模式打开文件会截断旧内容实际日志场景应使用a追加模式——这里重点在于演示锁的互斥语义。进程级并行Spawning Processes 与 multiprocessing原文档在 threading 之后还列了Spawning Processes与Multiprocessing两个小节标题但同样未展开正文。不过这一方向的机制其实已在concurrent.futures章节讲透ProcessPoolExecutor正是派生多个进程、通过可 pickle 的 IPC 传递任务与结果的进程级并行实现而标准库multiprocessing模块则是更底层的进程管理 API。结合 GIL 的分析进程方案是 CPU 密集任务绕过 GIL、真正利用多核的通用答案其代价是进程创建与 IPC 开销高于线程且数据必须在进程间序列化传递。总结为你的 Python 应用选择加速路线综合全章可以沉淀出一条清晰的决策流程先定位瓶颈是 CPU 密集循环、数值计算还是 I/O 密集网络、磁盘文档强调必须理解 GIL 对你应用的具体影响、机器核数与瓶颈位置。CPU 密集的纯 Python 代码优先尝试换解释器PyPy或对热点函数用Cython做强类型化编译实测可达约 1017 倍加速若热点在底层库里则参考 clibs.rst 用 ctypes/CFFI 直接对接。I/O 密集任务用ThreadPoolExecutor或threading做并发阻塞期间 GIL 自动让出线程池高效共享资源记得用Lock防数据竞争。CPU 密集且需多核并行用ProcessPoolExecutor或底层multiprocessing绕开 GIL但数据必须可 pickle。写任何 C 扩展务必正确向解释器注册线程遵守 C-API 的 GIL 管理约定。这套从解释器选择到C 扩展编译再到线程/进程并发的完整工具箱正是本仓库 Speed 场景章节 想传达的核心Python 的性能问题没有银弹但有清晰的分诊路径——理解 GIL 是前提对症下药是关键。更多背景可继续阅读 which-python.rst解释器选型与 clibs.rstC/C 接口。【免费下载链接】python-guidePython best practices guidebook, written for humans.项目地址: https://gitcode.com/gh_mirrors/py/python-guide创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考