Влияет ли большой и обширный PYTHONPATH на производительность? - PullRequest
0 голосов
/ 29 июня 2018

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

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

from folder1.folder2.folder3 import foo

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

from folder3 import foo

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

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

Ответы [ 3 ]

0 голосов
/ 29 июня 2018

Таким образом, компромисс производительности между наличием множества различных каталогов в вашем 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 ... но мне это кажется слишком большим шагом.

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

0 голосов
/ 29 июня 2018

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

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

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

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

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

0 голосов
/ 29 июня 2018

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

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

...