Можно ли настроить Python для кэширования поиска в каталоге sys.path?

Мы проводили много тестов Python, работающих по удаленному соединению. Программа запускается за пределами предприятия, но доступ к дискам на месте. Мы работаем под RHEL6. Мы смотрели простую программу с strace. Похоже, он тратит много времени на выполнение stat и открывает файлы, чтобы узнать, есть ли они там. За удаленным подключением это дорого. Есть ли способ настроить 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 >

Если он кэшировал каталог, ему не пришлось бы ставить файл, чтобы увидеть, есть ли он.

Ответ 1

Вы можете избежать этого, перейдя на Python 3.3 или заменив стандартную систему импорта альтернативой. В разговоре strace, который я дал две недели назад в PyOhio, я обсуждаю неудачную производительность O (nm) (для n каталогов и возможных суффиксов) старого механизма импорта; начинайте с этот слайд.

Я демонстрирую, как easy_install плюс веб-среда с поддержкой Zope генерирует 73,477 системных вызовов просто для того, чтобы сделать достаточно импорта для запуска и запуска.

После быстрой установки bottle в virtualenv на моем ноутбуке, например, я нахожу, что для Python требуется, чтобы для этого модуля было загружено ровно 1000 вызовов:

$ 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 или о модулях внутри пакетов (для чего потребуется выполнить lsdir() в подкаталогах записей sys.path). Но это, по крайней мере, иллюстрирует, что тысячи дополнительных вызовов можно устранить с помощью чего-то подобного и продемонстрировать направление, которое вы можете попробовать. Когда он не может найти модуль, вместо этого вместо него запускается обычный механизм импорта.

Интересно, есть ли хороший импортер кэширования, уже доступный на PyPI или где-то еще? Это похоже на то, что было бы написано сотни раз в разных магазинах. Я думал, что Noah Gift написал один и поместил его в сообщение в блоге или что-то в этом роде, но я не могу найти ссылку, подтверждающую мою память.

Изменить:, поскольку @ncoglan упоминает в комментариях, есть альфа-релиз backport новой системы импорта Python 3.3+ для Python 2.7, доступный в PyPI: http://pypi.python.org/pypi/importlib2 - к сожалению, похоже, что вопросник все еще застрял на 2.6.

Ответ 2

Я знаю, что это не совсем то, что вы ищете, но я все равно отвечу: D

Нет системы кэширования для каталогов sys.path, но zipimport создает индекс модулей внутри файла .zip. Этот индекс используется для ускорения поиска модуля.

Недостатком этого решения является то, что вы не можете использовать его с двоичными модулями (например .so) из-за отсутствия поддержки в dlopen(), который используется Python для загрузки этого типа модуля.

Другая проблема заключается в том, что некоторые модули (например, posixpath, используемые в вашем примере) загружаются интерпретатором CPython во время процесса начальной загрузки.

PS. Надеюсь, ты помнишь меня в PythonBrasil, когда я помог тебе надеть сумки с подарками Disney/Pixar: D

Ответ 3

В дополнение к использованию импортера или zipimport вы также должны рассмотреть возможность замораживания своего кода. Замораживание резко сократит статические вызовы.

часть Python: https://wiki.python.org/moin/Freeze сторонняя: http://cx-freeze.readthedocs.org/en/latest/

Замерзание рудиментарных script отброшенных статистических данных от 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 и его кэширование помогут вам).