我是工作在扩展的简单类dict
,我意识到键查找和使用pickle
都非常缓慢。
我认为这是我班上的一个问题,所以我做了一些琐碎的基准测试:
(venv) marco@buzz:~/sources/python-frozendict/test$ python --version
Python 3.9.0a0
(venv) marco@buzz:~/sources/python-frozendict/test$ sudo pyperf system tune --affinity 3
[sudo] password for marco:
Tune the system configuration to run benchmarks
Actions
=======
CPU Frequency: Minimum frequency of CPU 3 set to the maximum frequency
System state
============
CPU: use 1 logical CPUs: 3
Perf event: Maximum sample rate: 1 per second
ASLR: Full randomization
Linux scheduler: No CPU is isolated
CPU Frequency: 0-3=min=max=2600 MHz
CPU scaling governor (intel_pstate): performance
Turbo Boost (intel_pstate): Turbo Boost disabled
IRQ affinity: irqbalance service: inactive
IRQ affinity: Default IRQ affinity: CPU 0-2
IRQ affinity: IRQ affinity: IRQ 0,2=CPU 0-3; IRQ 1,3-17,51,67,120-131=CPU 0-2
Power supply: the power cable is plugged
Advices
=======
Linux scheduler: Use isolcpus=<cpu list> kernel parameter to isolate CPUs
Linux scheduler: Use rcu_nocbs=<cpu list> kernel parameter (with isolcpus) to not schedule RCU on isolated CPUs
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
x = {0:0, 1:1, 2:2, 3:3, 4:4}
' 'x[4]'
.........................................
Mean +- std dev: 35.2 ns +- 1.8 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
class A(dict):
pass
x = A({0:0, 1:1, 2:2, 3:3, 4:4})
' 'x[4]'
.........................................
Mean +- std dev: 60.1 ns +- 2.5 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
x = {0:0, 1:1, 2:2, 3:3, 4:4}
' '5 in x'
.........................................
Mean +- std dev: 31.9 ns +- 1.4 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
class A(dict):
pass
x = A({0:0, 1:1, 2:2, 3:3, 4:4})
' '5 in x'
.........................................
Mean +- std dev: 64.7 ns +- 5.4 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python
Python 3.9.0a0 (heads/master-dirty:d8ca2354ed, Oct 30 2019, 20:25:01)
[GCC 9.2.1 20190909] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from timeit import timeit
>>> class A(dict):
... def __reduce__(self):
... return (A, (dict(self), ))
...
>>> timeit("dumps(x)", """
... from pickle import dumps
... x = {0:0, 1:1, 2:2, 3:3, 4:4}
... """, number=10000000)
6.70694484282285
>>> timeit("dumps(x)", """
... from pickle import dumps
... x = A({0:0, 1:1, 2:2, 3:3, 4:4})
... """, number=10000000, globals={"A": A})
31.277778962627053
>>> timeit("loads(x)", """
... from pickle import dumps, loads
... x = dumps({0:0, 1:1, 2:2, 3:3, 4:4})
... """, number=10000000)
5.767975459806621
>>> timeit("loads(x)", """
... from pickle import dumps, loads
... x = dumps(A({0:0, 1:1, 2:2, 3:3, 4:4}))
... """, number=10000000, globals={"A": A})
22.611666693352163
结果确实令人惊讶。虽然键查找为2x速度较慢,pickle
是5倍速度较慢。
怎么会这样?其他的方法,如get()
,__eq__()
和__init__()
,和遍历keys()
,values()
并且items()
是一样快dict
。
编辑:我看了Python 3.9的源代码,Objects/dictobject.c
似乎该__getitem__()
方法是由实现的dict_subscript()
。并且dict_subscript()
仅在缺少键的情况下才放慢子类的速度,因为子类可以实现__missing__()
并尝试查看它是否存在。但是基准是使用现有密钥。
但是我注意到了:__getitem__()
用flag定义METH_COEXIST
。并且__contains__()
,另一种慢2倍的方法具有相同的标志。从官方文档中:
该方法将代替现有定义加载。如果没有METH_COEXIST,则默认为跳过重复的定义。由于插槽包装程序是在方法表之前加载的,因此,例如sq_contains插槽的存在会生成一个名为contains()的包装方法, 并避免加载具有相同名称的相应PyCFunction。定义了标志后,PyCFunction将代替包装对象被加载,并与插槽共存。这很有用,因为对PyCFunctions的调用比对包装对象的调用进行了优化。
因此,如果我理解正确的话,理论上METH_COEXIST
应该加快速度,但这似乎有相反的效果。为什么?
编辑2:我发现了更多。
__getitem__()
和__contains()__
被标记为METH_COEXIST
,因为它们在PyDict_Type中被声明了两次。
它们都一次出现在插槽中tp_methods
,并在其中显式声明为__getitem__()
和__contains()__
。但是,官方文件说,tp_methods
是不是由子类继承。
因此,的子类dict
不会调用__getitem__()
,而是会调用子插槽mp_subscript
。实际上,mp_subscript
它包含在slot中tp_as_mapping
,它允许子类继承其子槽。
问题是,无论是__getitem__()
与mp_subscript
使用相同的功能,dict_subscript
。可能仅仅是它的继承方式减慢了它的速度吗?
len()
不慢2倍,但速度却一样?
len
应该为内置序列类型提供一条快速路径。我认为我无法为您的问题提供适当的答案,但这是一个很好的答案,因此希望有人比我更了解Python内部知识。
__contains__
实现阻止了用于继承的逻辑sq_contains
。
dict
,如果是,则直接调用C实现,而不是__getitem__
从中查找方法。对象的类。因此,您的代码执行两次dict查询,第一个对'__getitem__'
classA
成员字典中的键进行查找,因此可以预期它的速度大约是它的两倍。该pickle
解释可能是非常相似的。