摘要:
!python -V
Python 3.13.1
基本信息
6 个新特性
- 解释器吸收了 PyPy 项目 REPL 的一些特性
- 错误提示改进:使用彩色、位置更精准
- 自由线程:不受 GIL 管理的 —— 实验特性,默认关闭
- JIT 终于来了:即时编译器,编译机器码 —— 实验特性,需打开编译开关
locals()
修改语义- iOS 和 Android 开始支持
新的交互式解释器(interpreter)
- Interpreter(解释器):负责解析、编译(将代码转换为字节码)并运行代码,解析的代码可以从
.py
文件,也可以是通过 REPL 得到的 stdin。 - REPL(read-eval-print loop,读取-求值-打印-循环):与 Interpreter 交互,即通过命令行或终端启动时的交互式编程环境。终端输入
python
、python3
、ipython
等可启动 REPLpython
、python3
是 Interpreter 的一部分,即:Interpreter 的交互模式。ipython
是 3rd 的独立、增强 REPL,所以有独立的版本号。
$ python -c "import IPython;print(IPython.__version__)"
8.30.0
日常还会提到 shell 或 python shell,这个概念与 REPL 有一些细微差别:
- shell 一个更广泛的概念,可以包括图形界面或其他类型的命令行接口(比如 zsh)。
- REPL 具体描述与 python interpreter 其操作过程、交互流程。
- 两个概念大部分语境下是相同的。
来看看新的 interpreter 的交互模式,即:interactive shell 有什么新功能:
1、 具有历史保存的多行编辑
首先多行编辑更友好了:
$ python
Python 3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foobar():
...
...
... print("hello")
...
>>>
foobar():
后回车会直接缩进 —— 3.12 版本是没有缩进的。
连续回车并不会退出函数定义,直到 print...
写下函数体之后,再 2 次换行才退出 —— 3.12 是始终无法多次换行的
其次是有历史记录的多行编辑:
同样操作在 3.12 中是不行的。
2、 对 REPL 专属的命令如 help, exit 和 quit 的直接支持,无需以函数形式调用它们。
$ python
Python 3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> quit
以前 3.0+ 则必须 quit()
加上括号,这又回退到 2.x 版本的方式了,呵呵。
3、 Prompts and tracebacks 彩色显示
五彩斑斓的 output,而 3.12 则都是黑白色的:
最后,如果你不喜欢 3.13 中新的 REPL,还想用以前的,可以通过环境变量实现:
$ export PYTHON_BASIC_REPL=1
$ python
Free-threaded CPython
Free-threaded 即禁用 GIL(全局解释器锁)的模式,该模式在 3.13 中仍是实验性质,默认关闭,需要 --disable-gil
来手动打开。
$ ./configure --disable-gil
$ make -j$(nproc)
$ ./python -VV
Python 3.13.1 experimental free-threading build (main, Dec 23 2024, 18:23:38) [GCC 9.4.0]
这样编译得到 Free-threaded 模式的 python,python -VV
中的 experimental free-threading build
也是对此的说明。
使用 --disable-gil
编译出的 python 在运行中也可以使用 GIL,通过环境变量或参数来实现:
$ PYTHON_GIL=1
$ python foobar.py
或
$ python -X gil=1 foobar.py
gil=[0|1]
0 表示禁用 GIL,只能用在 --disable-gil
的 python 中;1 表示使能 GIL,可以用在任何 python 中。
我们用一个计算密集型函数验证一下 Free-threaded 模式的性能:
import sys
import threading
import time
def compute_heavy_task(n):
total = 0
for i in range(n):
total += i * i # 简单的平方和计算
return total
# 测试多线程性能的函数
def test_multithreading(num_threads, iterations_per_thread):
threads = []
start_time = time.time()
for _ in range(num_threads):
thread = threading.Thread(
target=compute_heavy_task, args=(iterations_per_thread,)
)
threads.append(thread)
thread.start()
for thread in threads:
thread.join() # 等待所有线程完成
end_time = time.time()
print(
f"Total execution time with {num_threads} threads: {end_time - start_time:.4f} seconds"
)
if __name__ == "__main__":
num_threads = 8
iterations_per_thread = 10_000_000
print(f"{sys.version}\n{num_threads}\n{iterations_per_thread}")
test_multithreading(num_threads, iterations_per_thread)
3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0]
8
10000000
Total execution time with 8 threads: 13.5425 seconds
$ python test_multithreading.py
3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0]
8
10000000
Total execution time with 8 threads: 11.5728 seconds
$ Python-3.13.1--disable-gil/python test_multithreading.py
3.13.1 experimental free-threading build (main, Dec 23 2024, 18:23:38) [GCC 9.4.0]
8
10000000
Total execution time with 8 threads: 2.5263 seconds
有 GIL 11.5s,无 GIL 的 2.4s —— 性能提示非常明显。
对于单线程(num_threads = 1
),性能提升就不明显了,甚至还有些微增长:
$ python test_multithreading.py
3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0]
1
10000000
Total execution time with 1 threads: 1.4707 seconds
$ Python-3.13.1--disable-gil/python test_multithreading.py
3.13.1 experimental free-threading build (main, Dec 23 2024, 18:23:38) [GCC 9.4.0]
1
10000000
Total execution time with 1 threads: 1.4772 seconds
去掉 GIL 是很多年前就提出的,一直都被诟病为 python 性能瓶颈主要杀手,但 3.13 才开始实验性质去掉,说明这个 GIL 还是有一些棘手的,比如去掉 GIL 后可能存在的问题:
- 内存管理问题:在自由线程模式下,一些对象可能永生,即这些对象不会被重新分配,其引用计数也不会被修改。这种设计是为了避免引用计数争夺,但可能导致内存使用量增加。
- 不安全的并发访问:自由线程模式下,对某些对象的并发访问是不安全的,脏数据等以前不存在的问题都会回来,需要用户来保障脏数据问题。
- C 扩展兼容性问题:python 调用 C 的库函数时,并发所有 C 函数都是并发安全的,这一块是 python 库开发者保障,还是交给用户保障 —— 这会是个很难纠扯清楚的事情,python 库开发者肯定会因为这个被骂好多年。被骂的狠了,说不定 3.14 就把这个 GIL 又强制加上了。
更多的关于去掉 GIL 的设计思路和方案,可参考 PEP-703
JIT 编译器
从 Python 创始至今,运行效率一直是短板和被人诟病, 除了 GIL 影响多线程效率,就是解释运行而不是编译运行,即:CPython 解释器将 Python 源代码编译成字节码,然后逐条解释执行 —— 这种简单的方式给 python 带来了跨操作系统、易用、繁荣的场景,但性能已一直被诟病。
从用户的代码到最终 CPU 上运行有几种方式:
- AOT(Ahead-Of-Time)编译:提前编译,即:编译阶段就编译成机器码,运行时直接运行机器码 —— C/C++ 就是典型的 AOT。
- JIT(Just-In-Time)编译:即时编译,即:编译阶段输出中间码,运行时即时编译成机器码进行执行(首次运行会慢点,但多次运行则效率与 AOT 相同)—— Java、C# 是这种。
- 无机器码编译:即:编译阶段输出中间码,运行时解释器逐行解释并运行中间码,没有机器码生成和执行过程 —— 3.12 及之前的 Python 是这种。
没有 JIT 与 执着于 GIL 一样,都被喷了很多年了,今天他俩一起来了!
仍然也是实验性质,默认关闭,需要自己通过 --enable-experimental-jit
打开:
./configure --enable-experimental-jit
make -j$(nproc)
make install
其实,Python 多年来也出现了一些 3rd 的 JIT 将 python 字节码翻译成机器码,比如 PyPy、GraalPy、Numba、Cython…… 但 python 官方的 CPython 一直没有出手,虽然前几年也预告过要做,但始终不见发布。
但其实也并不是直接编译成机器码这个方式,因为在运行时编译成机器码——没有完整的 GCC、Clang 等 ToolChain 是不现实的,所以 cpython JIT 还加入了:局部优化、分级特化、拷贝补丁式机器码。
其内部流程大致如下:
- 将从特化的 第 1 层级字节码 开始。—— 参阅 3.11 新特性了解详情。
- 当第 1 层级字节码达到足够热度,它将被翻译为新的纯内部的中间表示形式 (IR),称为 第 2 层级 IR,有时也称为微操作码 ("uops")。
- 第 2 层级 IR 使用与第 1 层级相同的基于栈的虚拟机,但其指令格式更适合被翻译为机器码。
- 在第 2 层级 IR 被解释或翻译为机器码之前,我们会预先应用一些优化通路。
- 虽然第 2 层级解释器存在,但它主要用于对优化管线的先前阶段进行调试。可通过为 Python 配置
--enable-experimental-jit=interpreter
选项启用第 2 层级解释器。 - 启用 JIT 时,经 优化的第 2 层级 IR 将被翻译为机器码后再执行。
- 这个机器码翻译过程使用了名为 拷贝并打补丁 的技巧。 它没有运行时依赖,但增加了构建时对 LLVM 的依赖。
即使如此,对特定硬件或 OS 的支持也是困难的,所以官方做了平台优先级分级,一级平台:
- aarch64-apple-darwin/clang
- aarch64-pc-windows/msvc [1]
- aarch64-unknown-linux-gnu/clang [2]
- aarch64-unknown-linux-gnu/gcc [2]
- i686-pc-windows-msvc/msvc
- x86_64-apple-darwin/clang
- x86_64-pc-windows-msvc/msvc
- x86_64-unknown-linux-gnu/clang
- x86_64-unknown-linux-gnu/gcc
其他平台的可能就算是二级公民了,可能永远不会得到 JIT 支持。
缺陷也有,比如:
- C 代码的分析器和调试器目前似乎无法回溯 JIT 帧,导致在 debug 过程中跟踪不住 JIT 帧。
- JIT 在运行时生成大量可执行数据。这为 CPython 引入了一个潜在的新攻击面。
更多的内容参考:PEP-744
网上也有网友对早期的 3.13.0b1 版本做过 JIT、GIL 的性能测试,比如:
https://github.com/lip234/python_313_benchmark
name | --disable-gil | --enable-experimental-jit |
---|---|---|
stock | N | N |
jit | N | Y |
nogil | Y | N |
jit+nogil | Y | Y |
测试的结果和我的想象有一些不一致,可能是 3.13 的早期版本与正式版本有差异,也可能是测试用例有问题,还有可能是这两个特性可能还不成熟。
iOS、Android 的支持
两个 PEP 都已经在 3.13 中实现。
在 Android 上,没有将安装作为系统资源的概念。软件分发的唯一单位是“应用程序”。此外,也没有控制台可供您运行可执行文件,或与 Python REPL 进行交互。
因此,在 iOS/Android 上使用 Python 的唯一方法是 embedded mode:即编写一个原生 Android 应用程序,使用 libpython 嵌入一个 Python 解释器,并使用 Python embedding API 调用 Python 代码。然后,完整的 Python 解释器、标准库和所有的 Python 代码都会被打包到您的应用程序中,供其自己使用。
简单说:就是实现 C/C++ 调用 Python —— 这与我们平常在 Python 中调用 C/C++ 趁好相反。
特别要注意:Python 标准库对 Android 有一些明显的遗漏和限制。
但 iOS 还有一些特殊:
- 有更友好的 3rd 开发环境:
- Pythonista :一个完整的开发环境,可以在 iPad 或 iPhone 上编写包括第三方库和系统集成在内的 Python 脚本。
- Pyto :为运行 Python3 提供了完整的开发环境,包括许多第三方库和 iPad 或 iPhone 上的系统集成。