Python可以配置缓存sys.path目录查找吗?

12
我们一直在对Python通过远程连接运行的性能进行基准测试。程序在外部运行,但访问内部磁盘。我们在RHEL6下运行一个简单的strace程序,发现它花费了很多时间在文件上执行stat和open操作,以查看文件是否存在。在远程连接中,这是昂贵的。有没有办法配置Python一次读取目录内容并缓存其列表,以便不必再次检查呢?
示例程序test_import.py:
import random
import itertools

我运行了以下命令:

$ strace -Tf python test_import.py >& strace.out
$ grep '/usr/lib64/python2.6/' strace.out | wc
331    3160   35350

所以它大约要在那个目录中搜索331次。其中很多结果会像这样:

stat ( "/usr/lib64/python2.6/posixpath", 0x7fff1b447340 ) = -1 ENOENT ( No such file or directory ) < 0.000009 >

如果缓存了目录,就不需要对文件进行状态检查以确定它是否存在。


2
你能提供更多关于它正在查找哪些文件的信息吗?你确定这是由于模块查找引起的吗?如果导入一个模块超过一次,应该使用sys.modules中缓存的模块对象,因此如果这是查找的本质,可能没有太多可获得的。 - BrenBarn
谢谢您要求澄清。我认为这不是关于模块查找,但它们也会受益于缓存。我将在问题中添加更多信息。 - Paul Hildebrandt
1
远程连接是什么意思?运行Python程序的计算机是否与存储Python包的计算机不同? - Nick ODell
2
另外,你是否只使用2.6版本?在2.7/3.2版本中,这方面有一些小的改进,然后在3.3版本中,整个导入系统被重写(90%纯Python)。例如,您可以轻松配置3.3版本,即使.py文件在其他地方,也可以将其设置为使用本地磁盘上的.pyc目录,并将其连接到忽略或复制任何远程.pyc文件...但是在2.6版本上,这要困难得多,甚至可能不可能。 - abarnert
1
或者……使用 virtualenv 创建一个独立的虚拟环境怎么样?如果你的库太大,本地驱动器太小,那可能不是一个选项,但如果是的话,那应该完全避免这个问题。 - abarnert
显示剩余2条评论
3个回答

9
您可以通过升级到Python 3.3或替换标准的导入系统以避免这种情况。在我两周前在PyOhio的strace演讲中,我讨论了旧的导入机制的不幸的O(nm)性能(对于n个目录和m个可能的后缀),请从这张幻灯片开始。我展示了如何使用easy_install加上Zope-powered web框架仅产生73,477个系统调用,就足以进行足够的导入并运行。
例如,在我的笔记本电脑上快速安装bottle虚拟环境后,我发现仅需要1,000次调用才能使Python导入该模块并运行。
$ strace -c -e stat64,open python -c 'import bottle'
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000179           0      1519      1355 open
  0.00    0.000000           0       475       363 stat64
------ ----------- ----------- --------- --------- ----------------
100.00    0.000179                  1994      1718 total

然而,如果我跳转到os.py文件中,我可以添加一个缓存导入器,即使是使用非常简单的实现,也可以将未命中次数减少近千次:

$ strace -c -e stat64,open python -c 'import bottle'
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000041           0       699       581 open
  0.00    0.000000           0       301       189 stat64
------ ----------- ----------- --------- --------- ----------------
100.00    0.000041                  1000       770 total

我选择研究os.py模块,因为使用strace可以看到它是Python导入的第一个模块,而且我们越早安装自己的导入器,Python就能少导入一些标准库模块,避免使用原来那个缓慢可怕的机制。

# Put this right below "del _names" in os.py

class CachingImporter(object):

    def __init__(self):
        self.directory_listings = {}

    def find_module(self, fullname, other_path=None):
        filename = fullname + '.py'
        for syspath in sys.path:
            listing = self.directory_listings.get(syspath, None)
            if listing is None:
                try:
                    listing = listdir(syspath)
                except OSError:
                    listing = []
                self.directory_listings[syspath] = listing
            if filename in listing:
                modpath = path.join(syspath, filename)
                return CachingLoader(modpath)

class CachingLoader(object):

    def __init__(self, modpath):
        self.modpath = modpath

    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]
        import imp
        mod = imp.new_module(fullname)
        mod.__loader__ = self
        sys.modules[fullname] = mod
        mod.__file__ = self.modpath
        with file(self.modpath) as f:
            code = f.read()
        exec code in mod.__dict__
        return mod

sys.meta_path.append(CachingImporter())

当然,这个方法还有一些不足之处——它不会尝试检测.pyc文件、.so文件或Python可能寻找的其他扩展名。它也不知道__init__.py文件或包内部的模块(这需要在sys.path条目的子目录中运行lsdir())。但至少它说明了可以通过类似这样的东西消除数千个额外的调用,并演示了您可能尝试的方向。当它找不到一个模块时,正常的导入机制会启动。 我想知道是否已经有一个很好的缓存导入器可在PyPI或其他地方使用?这似乎是各种公司已经写过数百次的东西。我认为Noah Gift曾经写过一个并将其放在博客文章或其他什么地方,但我找不到一个链接来证实我的记忆。 编辑:如@ncoglan在评论中提到的那样,在PyPI上有一个新的Python 3.3+导入系统的alpha版迁移给Python 2.7使用:http://pypi.python.org/pypi/importlib2——不幸的是,问答者仍然停留在2.6上。

1
这非常有帮助。谢谢Brandon。我们将使用它运行一些测试。 - Paul Hildebrandt
2
Eric Snow在PyPI上有一个整个Python 3导入系统的实验性回溯版本:https://pypi.python.org/pypi/importlib2 - ncoghlan
1
谢谢Nick,我们正在Python 2.7下调查问题,并检查Eric编写的库。 - Paul Hildebrandt
1
鉴于importlib的强大测试套件,我有信心认为importlib2是可靠的。我只是将其定为“alpha”版本,因为我还没有机会对代码进行最后一次离开一段时间的审查。 - Eric Snow

1
我知道这不完全是您要找的,但我还是会回答:D sys.path目录没有缓存系统,但zipimport会在.zip文件内创建模块索引。该索引用于加速模块查找。
这种解决方案的缺点是,由于Python用dlopen()来加载此类模块,因此无法与二进制模块(例如.so)一起使用。
另一个问题是,某些模块(例如在您的示例中使用的posixpath)在CPython解释器启动过程中被加载。
PS. 我希望您还记得我在PythonBrasil时帮助您装Disney / Pixar纪念品袋:D

我还记得并感谢你的帮助,无论是过去还是现在。 :-) 我很感激你指出了zipimport。你说得对,它不能解决更大的问题,但可能会有所帮助。 - Paul Hildebrandt

1

除了使用导入器或zipimport之外,您还应考虑冻结您的代码。冻结将大大减少stat调用。

Python部分:https://wiki.python.org/moin/Freeze 第三方:http://cx-freeze.readthedocs.org/en/latest/

一个简单脚本的冻结可使统计调用从232降至88。

$ strace -c -e stat64,open python2 hello.py
hello
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000011           0       232       161 open
------ ----------- ----------- --------- --------- ----------------
100.00    0.000011                   232       161 total
$ strace -c -e stat64,open ./hello
hello
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  -nan    0.000000           0        88        73 open
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    88        73 total

您仍然会受到sys.path中条目数量的影响(但这就是importlib2及其缓存可以帮助您的地方)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接