Влияет ли большой и обширный PYTHONPATH на производительность?

Допустим, у вас есть проект, который имеет несколько уровней папок и в разных местах, чтобы сделать импортные звонки более чистыми, люди изменили PYTHONPATH для всего проекта.

Это означает, что вместо того, чтобы говорить:

from folder1.folder2.folder3 import foo

теперь они могут сказать

from folder3 import foo

и добавьте folder1/folder2 в PYTHONPATH. Вопрос здесь заключается в том, что если вы продолжите в том же духе и добавите в PYTHONPATH большое количество путей, будет ли это значительным или значительным ударом по производительности?

Чтобы добавить некоторый смысл масштаба, с точки зрения производительности, я спрашиваю в терминах миллисекунд как минимум (то есть: 100 мс? 500 мс?)

3 ответа

Решение

Таким образом, компромисс производительности между наличием множества различных каталогов в вашем PYTHONPATH а наличие глубоко вложенных структур пакетов будет видно в системных вызовах. Предполагая, что у нас есть следующие структуры каталогов:

bash-3.2$ tree a
a
└── b
    └── c
        └── d
            └── __init__.py
bash-3.2$ tree e
e
├── __init__.py
├── __init__.pyc
└── f
    ├── __init__.py
    ├── __init__.pyc
    └── g
        ├── __init__.py
        ├── __init__.pyc
        └── h
            ├── __init__.py
            └── __init__.pyc

Мы можем использовать эти структуры и strace Программа для сравнения и сопоставления системных вызовов, которые мы генерируем для следующих команд:

strace python -c 'from e.f.g import h'
PYTHONPATH="./a/b/c:$PYTHONPATH" strace python -c 'import d'

Много записей PYTHONPATH

Таким образом, компромисс здесь - это системные вызовы во время запуска по сравнению с системными вызовами во время импорта. Для каждой записи в PYTHONPATH, python сначала проверяет, существует ли каталог:

stat("./a/b/c", {st_mode=S_IFDIR|0776, st_size=4096, ...}) = 0
stat("./a/b/c", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0

Если каталог существует (он... обозначен 0 справа), при запуске интерпретатора Python будет искать несколько модулей. Для каждого модуля он проверяет:

stat("./a/b/c/site", 0x7ffd900baaf0)    = -1 ENOENT (No such file or directory)
open("./a/b/c/site.x86_64-linux-gnu.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("./a/b/c/site.so", O_RDONLY)       = -1 ENOENT (No such file or directory)
open("./a/b/c/sitemodule.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("./a/b/c/site.py", O_RDONLY)       = -1 ENOENT (No such file or directory)
open("./a/b/c/site.pyc", O_RDONLY)      = -1 ENOENT (No such file or directory)

Каждый из них не выполняется, и он переходит к следующей записи в пути в поисках модуля для заказа. Мой 3.5 intepretter просмотрел 25 модулей таким образом, создавая 152 системные вызовы при запуске по новой PYTHONPATH запись.

Глубокая структура упаковки

Глубокая структура пакета не платит штраф при запуске интерпретатора, но когда мы импортируем из глубоко вложенной структуры пакета, мы видим разницу. В качестве основы, вот простой импорт d/__init__.py от a/b/c каталог в нашем PYTHONPATH:

stat("/home/matt/a/b/c/d", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
stat("/home/matt/a/b/c/d/__init__.py", {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
stat("/home/matt/a/b/c/d/__init__", 0x7ffd900ba990) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__.x86_64-linux-gnu.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__module.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__.py", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
open("/home/matt/a/b/c/d/__init__.pyc", O_RDONLY) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=117, ...}) = 0
read(4, "\3\363\r\n\17\3105[c\0\0\0\0\0\0\0\0\1\0\0\0@\0\0\0s\4\0\0\0d\0"..., 4096) = 117
fstat(4, {st_mode=S_IFREG|0664, st_size=117, ...}) = 0
read(4, "", 4096)                       = 0
close(4)                                = 0
close(3)                                = 0

В основном то, что это делает, ищет d пакет или модуль. Когда он находит d/__init__.py он открывает его, а затем открывает d/__init__.pyc и читает содержимое в память перед закрытием обоих файлов.

С нашей глубоко вложенной структурой пакета мы должны повторить эту операцию еще 3 раза, что хорошо для 15 Системные вызовы на каталог в общей сложности еще 45 системных вызовов. Хотя это меньше половины количества вызовов, добавленных путем добавления пути к нашему PYTHONPATH, read вызовы потенциально могут занимать больше времени, чем другие системные вызовы (или требовать больше системных вызовов), в зависимости от размера __init__.py файлы.

TL; DR

Принимая все это во внимание, эти различия почти наверняка не настолько существенны, чтобы перевесить преимущества дизайна желаемого решения.

Это особенно верно, если ваши процессы работают долго (например, веб-приложение), а не недолговечно.

Мы можем уменьшить системные вызовы:

  1. Удаление посторонних PYTHONPATH записи
  2. Предварительно скомпилируйте ваш .pyc файлы, чтобы избежать необходимости их записи
  3. Сохраняйте структуру пакета плоской

Мы могли бы значительно повысить производительность, удалив py файлы, чтобы они не читались в целях отладки вместе с вашими файлами PYC... но это кажется мне слишком большим шагом.

Надеюсь, что это полезно, это, вероятно, гораздо более глубокое погружение, чем необходимо.

Это самая ужасная идея в истории.

Во-первых, конечно, потому что это делает код сложнее для чтения и рассуждений. Подождите, "folder3", откуда это??? Кроме того, потому что, если два пакета определяют подмодуль с одинаковым именем, то какой из них вы получите при импорте, зависит от порядка в вашей PYTHONPATH. И как только вы переставили PYTHONPATH, чтобы получить "moduleX" из "packageX", а не из "packageY", тогда кто-то добавил "moduleY" в "packageX", который скрывает "moduleY" от "packageY". И тогда ты облажался...

Но это только менее раздражающая часть...

Если у вас есть один модуль, использующий from folder1.folder2.folder3 import foo и другое использование from folder3 import fooвы получите два отдельных объекта модуля (два экземпляра вашего модуля) в sys.modules - и все объекты, определенные в этих модулях, также дублируются (два экземпляра, разные идентификаторы), и теперь у вас есть программа, которая начинает вести себя наиболее хаотично, когда требуется тестирование личности. И так как обработка исключений опирается на идентичность, если foo Это исключение, в зависимости от того, какой экземпляр модуля вызвал его, а какой пытается его перехватить, тест либо будет успешным, либо потерпит неудачу без заметного паттерна.

Удачи в отладке этого...

Это вряд ли повлияет на производительность, если вы не добавляете пути на медленных дисках. Но это может иметь незначительный эффект.

Проблема, с которой вы, скорее всего, столкнетесь, добавив слишком много PYTHONPATH Это конфликты модулей, когда в разных местах один и тот же модуль, но разные версии.

Другие вопросы по тегам