为什么在Python 3中未编译的重复使用的正则表达式这么慢?

17 投票
1 回答
1134 浏览
提问于 2025-04-17 15:08

在回答这个问题时(并且看过这个类似问题的回答),我以为我了解Python是如何缓存正则表达式的。

但我决定测试一下,比较两种情况:

  1. 先编译一个简单的正则表达式,然后使用这个编译好的正则表达式10次。
  2. 直接使用一个未编译的正则表达式10次(我本以为性能会稍微差一些,因为这个正则表达式需要先编译一次,然后缓存,再查找9次)。

然而,结果让我大吃一惊(在Python 3.3中):

>>> import timeit
>>> timeit.timeit(setup="import re", 
... stmt='r=re.compile(r"\w+")\nfor i in range(10):\n r.search("  jkdhf  ")')
18.547793477671938
>>> timeit.timeit(setup="import re", 
... stmt='for i in range(10):\n re.search(r"\w+","  jkdhf  ")')
106.47892003890324

速度慢了超过5.7倍!在Python 2.7中,速度也慢了2.5倍,这也比我预期的要多。

那么,正则表达式的缓存机制在Python 2和3之间有变化吗?文档似乎没有提到这一点。

1 个回答

27

这段代码已经发生了变化。

在Python 2.7中,缓存是一个简单的字典。如果存储的项目超过了_MAXCACHE的数量,缓存会在存储新项目之前被清空。查找缓存的过程只需要构建一个简单的键,然后测试这个字典,具体可以参考2.7版本的_compile()

在Python 3.x中,缓存被替换成了@functools.lru_cache(maxsize=500, typed=True)装饰器。这个装饰器做了很多额外的工作,包括线程锁、调整缓存的LRU队列以及维护缓存统计信息(可以通过re._compile.cache_info()访问)。具体可以查看3.3.0版本的_compile()functools.lru_cache()

其他人也注意到了同样的速度变慢,并在Python的bug跟踪系统中提交了问题16389。我预计3.4版本会更快,要么是lru_cache的实现得到了改善,要么是re模块会再次使用自定义缓存。

更新:在修订版4b4dddd670d0 (hg) / 0f606a6 (git)中,缓存的变化被恢复到了3.1中的简单版本。Python版本3.2.4和3.3.1包含了这个修订。

从那时起,在Python 3.7中,模式缓存被更新为基于普通dict自定义FIFO缓存实现(依赖于插入顺序,与LRU不同的是,在驱逐项目时不考虑缓存中已有项目的使用频率)。

撰写回答