庞大而庞大的 PYTHONPATH 会影响性能吗?
Does a large and expansive PYTHONPATH affect performance?
假设您有一个项目,其中有多个级别的文件夹在不同的地方进行,为了使导入调用更清晰,人们已经修改了整个项目的 PYTHONPATH。
这意味着不是说:
from folder1.folder2.folder3 import foo
他们现在可以说
from folder3 import foo
并将 folder1/folder2 添加到 PYTHONPATH。这里的问题是,如果您继续这样做,并且将大量路径添加到 PYTHONPATH,这是否会对性能产生明显或显着的影响?
为了增加一些规模感,在性能方面,我要求至少以毫秒为单位(即:100 毫秒?500 毫秒?)
除非您在慢速驱动器位置附加路径,否则不太可能对性能产生影响。但这很可能会产生微不足道的影响。
将太多位置附加到 PYTHONPATH
最有可能遇到的问题是模块冲突,即不同位置具有相同模块但版本不同。
因此,在您的 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 解释器以这种方式查找了 25 个模块,在每个新 PYTHONPATH
条目启动时生成增量 152
系统调用。
深层封装结构
深层包结构不会对解释器启动造成任何影响,但是当我们从深层嵌套的包结构中导入时,我们确实看到了差异。作为基准,这里是 d/__init__.py
从我们的 PYTHONPATH
:
中的 a/b/c
目录简单导入
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\r\n05[c[=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=]@[=14=][=14=][=14=]s[=14=][=14=][=14=]d[=14=]"..., 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
考虑到所有这些因素,这些差异几乎肯定不会 material 足以超过您所需解决方案的设计优势。
如果您的进程是长期的 运行(如网络应用程序)而不是短暂的,则尤其如此。
我们可以通过以下方式减少系统调用:
- 正在删除任何无关的
PYTHONPATH
条目
- 预编译您的
.pyc
文件以避免需要编写它们
- 保持包结构平坦
我们可以通过删除您的 py
文件来更大幅度地提高性能,这样它们就不会与您的 PYC 文件一起被读取用于调试目的......但这对我来说似乎太过分了。
希望这有用,它可能比必要的深入得多。
这是有史以来最糟糕的想法,真的。
首先当然是因为它使代码更难阅读和推理。等等,'folder3',这是从哪里来的???。还因为如果两个包定义了一个同名的子模块,导入时得到的是哪一个取决于 PYTHONPATH 中的顺序。一旦你重新安排了你的 PYTHONPATH,这样你就可以从 "packageX" 而不是 "packageY" 得到 "moduleX",然后有人在 "packageX" 下添加一个 "moduleY" 来隐藏 [=29] =] 来自 "packageY"。然后你就完蛋了...
但是这只是不那么烦人的部分...
如果你有一个模块使用 from folder1.folder2.folder3 import foo
而另一个模块使用 from folder3 import foo
,你最终会在 sys.modules
中得到两个不同的模块对象(你的模块的两个实例)——以及所有在这些模块中定义的对象也被复制(两个实例,不同的 id),现在你有一个程序,只要涉及到一些身份测试,它就会开始以最不稳定的方式运行。并且由于异常处理依赖于身份,如果 foo
是异常,则取决于引发它的模块实例以及试图捕获它的模块实例,测试将成功或失败,没有可辨别的模式。
祝调试顺利...
假设您有一个项目,其中有多个级别的文件夹在不同的地方进行,为了使导入调用更清晰,人们已经修改了整个项目的 PYTHONPATH。
这意味着不是说:
from folder1.folder2.folder3 import foo
他们现在可以说
from folder3 import foo
并将 folder1/folder2 添加到 PYTHONPATH。这里的问题是,如果您继续这样做,并且将大量路径添加到 PYTHONPATH,这是否会对性能产生明显或显着的影响?
为了增加一些规模感,在性能方面,我要求至少以毫秒为单位(即:100 毫秒?500 毫秒?)
除非您在慢速驱动器位置附加路径,否则不太可能对性能产生影响。但这很可能会产生微不足道的影响。
将太多位置附加到 PYTHONPATH
最有可能遇到的问题是模块冲突,即不同位置具有相同模块但版本不同。
因此,在您的 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 解释器以这种方式查找了 25 个模块,在每个新 PYTHONPATH
条目启动时生成增量 152
系统调用。
深层封装结构
深层包结构不会对解释器启动造成任何影响,但是当我们从深层嵌套的包结构中导入时,我们确实看到了差异。作为基准,这里是 d/__init__.py
从我们的 PYTHONPATH
:
a/b/c
目录简单导入
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\r\n05[c[=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=]@[=14=][=14=][=14=]s[=14=][=14=][=14=]d[=14=]"..., 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
考虑到所有这些因素,这些差异几乎肯定不会 material 足以超过您所需解决方案的设计优势。
如果您的进程是长期的 运行(如网络应用程序)而不是短暂的,则尤其如此。
我们可以通过以下方式减少系统调用:
- 正在删除任何无关的
PYTHONPATH
条目 - 预编译您的
.pyc
文件以避免需要编写它们 - 保持包结构平坦
我们可以通过删除您的 py
文件来更大幅度地提高性能,这样它们就不会与您的 PYC 文件一起被读取用于调试目的......但这对我来说似乎太过分了。
希望这有用,它可能比必要的深入得多。
这是有史以来最糟糕的想法,真的。
首先当然是因为它使代码更难阅读和推理。等等,'folder3',这是从哪里来的???。还因为如果两个包定义了一个同名的子模块,导入时得到的是哪一个取决于 PYTHONPATH 中的顺序。一旦你重新安排了你的 PYTHONPATH,这样你就可以从 "packageX" 而不是 "packageY" 得到 "moduleX",然后有人在 "packageX" 下添加一个 "moduleY" 来隐藏 [=29] =] 来自 "packageY"。然后你就完蛋了...
但是这只是不那么烦人的部分...
如果你有一个模块使用 from folder1.folder2.folder3 import foo
而另一个模块使用 from folder3 import foo
,你最终会在 sys.modules
中得到两个不同的模块对象(你的模块的两个实例)——以及所有在这些模块中定义的对象也被复制(两个实例,不同的 id),现在你有一个程序,只要涉及到一些身份测试,它就会开始以最不稳定的方式运行。并且由于异常处理依赖于身份,如果 foo
是异常,则取决于引发它的模块实例以及试图捕获它的模块实例,测试将成功或失败,没有可辨别的模式。
祝调试顺利...