VSCode 用 debugpy 调试,加载 `pinyin_dict` 和 `phrases_dict` 这两个步骤时,速度严重下降
运行环境
- 操作系统(Linux/macOS/Windows):Windows10 Ent 22H2 更新到最新(2024.03.06 未检测到有未更新的系统补丁)
- Python 版本:3.12.2
- pypinyin 版本:0.50.0
问题描述
在 VSCode 不做特别配置的情况下,调试(VSCode 应该是默认使用的 debugpy)引用了 pypinyin 的 python 代码,
在加载 pinyin_dict 和 phrases_dict 这两个步骤时,速度严重下降到不可接受的地步。
环境配置:win10 x64 企业版 22H2,VSCode 1.87.0,R7-5800H 3.20GHz,内存 16G,512G SSD。
同样是在 VSCode 中,以非调试模式运行相同的代码几乎感觉不到加载过程, 所以这并不是 pypinyin 代码的问题,应该是 debugpy 的调试机制严重拖慢了两个巨型 dict 的初始化过程。
在网络上搜了很久,发现没人提到过这个问题,请问这是个案吗? 应该如何在 VSCode 中调试引用了 pypinyin 的代码?
麻烦试试设置一下下面这两个环境变量,看看是否会有帮助:https://github.com/mozillazg/python-pinyin?tab=readme-ov-file#%E5%A6%82%E4%BD%95%E5%87%8F%E5%B0%91%E5%86%85%E5%AD%98%E5%8D%A0%E7%94%A8
换了种方式解决了,魔改了几行源代码。 主要思路是从 pickle 二进制加载那几个硕大的 dict 数据,这样能避免因为 debugpy 介入导致的巨大耗时。
- 添加了一个源文件
dict_save_load.py:
import os
import pickle
def pickle_path(py__file__):
return py__file__[:-2] + "pkl"
def save_data_of(py__file__, data):
fp = pickle_path(py__file__)
with open(fp, "wb") as f:
pickle.dump(data, f)
def try_load_data_of(py__file__):
fp = pickle_path(py__file__)
if not os.path.isfile(fp):
return None
with open(fp, "rb") as f:
return pickle.load(f)
- 原本的
pinyin_dict.py、phrases_dict.py、phrases_dict_large.py重命名为*_code.py,然后用以下代码替换了原本的内容:
pinyin_dict.py
from pypinyin import dict_save_load
pinyin_dict = dict_save_load.try_load_data_of(__file__)
if not pinyin_dict:
from pypinyin.pinyin_dict_code import pinyin_dict
dict_save_load.save_data_of(__file__, pinyin_dict)
phrases_dict.py
from pypinyin import dict_save_load
phrases_dict = dict_save_load.try_load_data_of(__file__)
if not phrases_dict:
from pypinyin.phrases_dict_code import phrases_dict
dict_save_load.save_data_of(__file__, phrases_dict)
phrases_dict_large.py
from pypinyin import dict_save_load
phrases_dict = dict_save_load.try_load_data_of(__file__)
if not phrases_dict:
from pypinyin.phrases_dict_large_code import phrases_dict
dict_save_load.save_data_of(__file__, phrases_dict)
这个方案的缺点是,用户第一次必须以非 debug 模式运行一下代码,以确保在本地生成 pickle 二进制数据。如果第一次以 debug 模式运行的话,因为本地不存在 pickle 数据,所以必须从源代码初始化那几个硕大的 dict,这会导致第一次就以 debug 模式运行在初始化 dict 的时候会非常慢(我的机器上大概要等3~5分钟左右,R7-5800H 3.2GHz,DDR5-16G)。
更完善的思路是,让用户在安装 pypinyin 这个包的时候,就在本地生成 pickle 二进制数据,后面就可以直接加载 pickle 二进制,避免了第一次如果是 debug 模式会被 debugpy 拖慢的问题。本来想照这个思路做一个PR来提交,但是我不大清楚 py 包安装的脚本要怎么加这个过程所以没敢乱动安装脚本。
麻烦试试设置一下下面这两个环境变量,看看是否会有帮助:https://github.com/mozillazg/python-pinyin?tab=readme-ov-file#%E5%A6%82%E4%BD%95%E5%87%8F%E5%B0%91%E5%86%85%E5%AD%98%E5%8D%A0%E7%94%A8
这个方法试过了但是貌似不管用,以 debugpy 启动调试后还是慢得不行,那两个 dict 实在太大了,除非环境变量开关能够免除这两个 dict 的初始化,不然就会被 debugpy 拖慢。
@qaqz111 感谢分享经验!
python3.10没这问题,但3.12问题就出现了,我是改了个用法来规避这个问题:
import subprocess
_name = '测试' command = ['pypinyin', '-s', 'FIRST_LETTER', _name]
result = subprocess.run(command, stdout=subprocess.PIPE, text=True)
_pinyin_list = result.stdout.strip().split()
print(_pinyin_list)
我在MacOS遇到同样问题 ,可能和python版本有关。步骤如下:
将最新版的 phrases_dict.py 文件复制到当前目录,然后分别用 python 3.12.4, python 3.11.7, python 3.9.7 执行 import phrases_dict,在3.9.7环境下几秒钟能执行完,但是 3.11.7 和 3.12.4 要卡几分钟,进程 CPU 持续100%。
这两个库遇到有类似问题: https://github.com/carpedm20/emoji/issues/280#issuecomment-1900270724 https://github.com/holoviz/colorcet/issues/121
@guyskk 感谢分享相关信息!如果时间允许的话,我争取这周末的时候参考大家提供的信息先发布一个优化这个问题的 beta 版本。
我在其他项目的 issue 里看到过一些相关信息,debugger 速度下降是因为对每一行代码插入了一些检查,以启用调试器的断点等功能,所以有一些库采用了另外一种迎合该机制的方法,将所有初始化代码写在一行之内,即不换行一次性写完,这样貌似也能避开被 debugger 拖慢速度的问题。 pypinyin 的初始化也可以试试类似的解法,牺牲可读性将所有初始化语句放在数十个超长的行内写完,直到 python 未来版本修正了这个问题再改回来。
这里面可能有多个问题。我写了一个测试脚本,可以参考测试各种方案。
import os
import time
from pathlib import Path
def gen_data(num_line):
ret = 'large_data = [\n'
for _ in range(num_line):
ret += ' [],\n'
ret += ']\n'
return ret
def test_gen_data(num_line):
name = f'large_data_{num_line}'
filepath = Path(f'{name}.py')
filepath.write_text(gen_data(num_line))
t0 = time.monotonic()
exec(f'import {name}')
t1 = time.monotonic()
cost = t1 - t0
return cost
def main():
os.system('rm -r __pycache__/')
os.system('rm large_data_*.py')
for num_line in range(2000, 20000 + 1, 2000):
cost = test_gen_data(num_line)
print(f'{num_line},{cost:.6f}')
if __name__ == '__main__':
main()
我在MacOS 命令行上运行,测试发现:
- large_data 每一行都是空列表,在 python 3.9.19 速度很快,python 3.10.0 性能严重下降,python 3.12.4 有所缓解,但还是很慢。
- 把列表里面的换行去掉(所有语句写在一行),速度没有明显变化。
- 把 large_data 里面的空列表换成空 tuple或str,所有python版本速度都很快。换成空dict,或者 list(),速度没有明显变化。
- 在 linux docker 容器中运行测试,没有任何问题,所有python版本速度都非常快。
我的问题解决了,原因是我的命令行环境默认配置了 PYTHONTRACEMALLOC=1,去掉这个配置后一切正常。
@guyskk 感谢分享相关信息!如果时间允许的话,我争取这周末的时候参考大家提供的信息先发布一个优化这个问题的 beta 版本。
发布了一个 dev 版本(改动详见 https://github.com/mozillazg/python-pinyin/pull/324 , Thanks @serfend ),麻烦大家有空的时候帮忙验证一下问题是否还存在,谢谢~
pip install pypinyin==0.52.0.dev1
我在维护 pypinyin 下游的一个包,在 pypinyin 0.51.0 版本运行 pytest --cov 的时候会遇到类似的问题(只 pytest 没有此问题):
$ PYTEST_DEBUG=1 pytest --cov tests/
[...]
early skip of rewriting module: mw2fcitx.exporters [assertion]
early skip of rewriting module: mw2fcitx.exporters.opencc [assertion]
early skip of rewriting module: pypinyin [assertion]
early skip of rewriting module: pypinyin.compat [assertion]
early skip of rewriting module: pypinyin.constants [assertion]
early skip of rewriting module: pypinyin.pinyin_dict [assertion]
[stuck]
在更新至 0.52.0.dev1 之后此问题解决。
我在维护
pypinyin下游的一个包,在 pypinyin 0.51.0 版本运行pytest --cov的时候会遇到类似的问题(只pytest没有此问题):$ PYTEST_DEBUG=1 pytest --cov tests/ [...] early skip of rewriting module: mw2fcitx.exporters [assertion] early skip of rewriting module: mw2fcitx.exporters.opencc [assertion] early skip of rewriting module: pypinyin [assertion] early skip of rewriting module: pypinyin.compat [assertion] early skip of rewriting module: pypinyin.constants [assertion] early skip of rewriting module: pypinyin.pinyin_dict [assertion] [stuck]在更新至
0.52.0.dev1之后此问题解决。
@outloudvi 感谢反馈。我后面抽空发一下正式的 0.52.0 版本。
正式的优化该问题的 0.52.0 版本已发布。