Tachyon分析器
我试用了Python 3.15中新增的"高频统计采样分析器" Tachyon ,看看能否提升 Pillow 图像处理库的运行速度。我首先编写了一个简单的脚本来打开图像:
python
import sys
from PIL import Image
im = Image.open(f"Tests/images/hopper.{sys.argv[1]}")
然后运行:
console
$ python3.15 -m profiling.sampling run --flamegraph /tmp/1.py png
Captured 35 samples in 0.04 seconds
Sample rate: 1,000.00 samples/sec
Error rate: 25.71
Flamegraph data: 1 root functions, total samples: 26, 169 unique strings
Flamegraph saved to: flamegraph_97927.html
由此生成以下火焰图:

整个过程耗时 40 毫秒,其中一半在 Image.py 的 open() 中完成。如果您访问交互式 HTML 页面,我们可以看到 open() 调用 preinit(),而 preinit() 又导入了 GifImagePlugin、BmpImagePlugin、PngImagePlugin 和 JpegImagePlugin(将鼠标悬停在 <module> 框上即可查看它们)。
我们只需要 PNG 格式,真的需要导入所有这些插件吗?
好的,我们来尝试另一种类型的图像:
console
$ python3.15 -m profiling.sampling run --flamegraph /tmp/1.py webp
Captured 59 samples in 0.06 seconds
Sample rate: 1,000.00 samples/sec
Error rate: 22.03
Flamegraph data: 1 root functions, total samples: 46, 256 unique strings
Flamegraph saved to: flamegraph_98028.html

嗯,耗时 60 毫秒,其中 80% 的时间在 open() 中,而这 80% 的时间又大部分在 init() 中。HTML页面显示它导入了 AvifImagePlugin、PdfImagePlugin、WebpImagePlugin、DcxImagePlugin、DdsImagePlugin 和 PalmImagePlugin。此外,preinit 还导入了 GifImagePlugin、BmpImagePlugin 和 PngImagePlugin。
再说一遍,既然我们只关心 WebP,为什么还要导入更多插件呢?
正在加载所有插件?
分析就到此为止,我们来看看代码。
当对图像进行open()或save()处理时,如果 Pillow 尚未初始化,我们会调用一个preinit()函数。该函数通过导入插件来加载五种格式的驱动程序:BMP、GIF、JPEG、PPM 和 PNG。
导入过程中,每个插件都会注册其文件扩展名、MIME 类型以及一些用于打开和保存的方法。
然后我们依次检查每个插件,看看哪个插件可以接受这张图片。Pillow 的大多数插件都是通过打开文件并检查前几个字节是否匹配某个特定的前缀来检测图片的。例如:
- GIF以
b"GIF87a"或b"GIF89a"开头。 - PNG以
b"\211PNG\r\n\032\n"开头(参考)。 - JPEG以
b"\xff\xd8\xff"开头,其中\xff\xd8表示"图像的开始",\xff表示下一个标记的开始(参考)。
如果这五个插件都不符合条件,我们就调用init(),它会导入剩余的 42 个插件。然后,我们逐个检查这些插件是否匹配。
这种情况至少从 2000 年发布的PIL 1.1.1版本开始就存在(这是我能查到的最早版本)。当时有 33 个内置插件,现在是 47 个。
懒加载
如果一个程序在其生命周期内只需要一两种图像格式,那么这样做就有点浪费了,尤其是对于命令行界面(CLI)之类的程序。运行时间较长的程序可能需要更多格式,但不太可能需要全部 47 种。
插件系统的一个好处是第三方可以创建自己的插件,但我们可以使用内置功能提高效率。
我提交了一个PR ,添加了文件扩展名到插件的映射。在调用 preinit() 或 init() 之前,我们可以先进行简单的查找,这样可以省去导入、注册和检查所有这些插件的步骤。
当然,我们可能会遇到没有扩展名或扩展名"错误"的图像,但这没关系;我估计这种情况很少见,而且无论如何我们都会回退到原来的 preinit() -> init() 流程。
合并 PR 后,以下是用于打开 PNG( HTML 页面)的新火焰图:

对于 WebP( HTML 页面):

火焰图的宽度已缩放至相同,但方框数量大大减少,这意味着现在的工作量也大大降低。处理时间从 40 和 60 毫秒降至 20 和 20 毫秒。
该 PR 包含一系列基准测试,结果显示,打开 PNG 图片(之前需要加载 5 个插件)的速度提升了 2.6 倍。打开 WebP 图片(之前需要加载全部 47 个插件)的速度提升了 14 倍。同样,保存 PNG 图片的速度提升了 2.2 倍,保存 WebP 图片的速度提升了 7.9 倍。太棒了!这些改进将在 Pillow 12.2.0 版本中实现。