私は拡張する単純なクラスに取り組んでいて、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
結果は本当に驚きです。キーのルックアップが遅く2倍ですが、pickle
ある5倍遅くなります。
どうすればいいの?他の方法、のようなget()
、__eq__()
と__init__()
、とオーバー反復keys()
、values()
およびitems()
の速さですdict
。
編集:私はPython 3.9のソースコードを調べましたObjects/dictobject.c
が、その__getitem__()
方法はによって実装されているようdict_subscript()
です。またdict_subscript()
、サブクラスは実装でき__missing__()
、存在するかどうかを確認しようとするため、キーがない場合にのみサブクラスの速度を低下させます。しかし、ベンチマークは既存のキーでした。
しかし、私は何かに気づきました:__getitem__()
フラグで定義されていますMETH_COEXIST
。また__contains__()
、2倍遅いもう1つの方法には同じフラグがあります。公式ドキュメントから:
メソッドは、既存の定義の代わりにロードされます。METH_COEXISTがない場合、デフォルトでは、繰り返される定義をスキップします。スロットラッパーはメソッドテーブルの前に読み込まれるため、たとえばsq_containsスロットが存在すると、contains()という名前のラップされたメソッドが生成さ れ、同じ名前の対応するPyCFunctionが読み込まれなくなります。フラグが定義されると、ラッパーオブジェクトの代わりにPyCFunctionが読み込まれ、スロットと共存します。PyCFunctionsの呼び出しは、ラッパーオブジェクトの呼び出しよりも最適化されているため、これは役立ちます。
したがって、私が正しく理解していれば、理論的にMETH_COEXIST
は速度が上がるはずですが、逆の効果があるようです。どうして?
編集2:私はもっと何かを発見しました。
__getitem__()
そして、__contains()__
としてフラグ付けされているMETH_COEXIST
彼らはPyDict_Typeで宣言されているので、2回。
これらは両方ともスロットに1回だけ存在し、tp_methods
ととして明示的に宣言され__getitem__()
てい__contains()__
ます。しかし、公式のドキュメントはそれtp_methods
がサブクラスによって継承されないことを述べています。
したがって、のサブクラスはをdict
呼び出さず__getitem__()
、サブスロットを呼び出しますmp_subscript
。実際、mp_subscript
はtp_as_mapping
、サブクラスがそのサブスロットを継承できるようにするslot に含まれています。
問題は、__getitem__()
とmp_subscript
が同じ関数を使用していることdict_subscript
です。それが遅くなるのはそれが継承された方法だけであることは可能ですか?
len()
、例えば、2倍遅くはないが同じ速度なのですか?
len
組み込みシーケンスタイプの高速パスが必要だと思っていました。私はあなたの質問に適切な答えを出すことができるとは思いませんが、それは良い答えなので、うまくいけば、私よりもPythonの内部について詳しい人が答えるでしょう。
__contains__
実装は、継承に使用されるロジックをブロックしていsq_contains
ます。
dict
、__getitem__
メソッドをルックアップする代わりにC実装を直接呼び出しますオブジェクトのクラス。したがって、コードは2つのdictルックアップを実行します。1つ'__getitem__'
はクラスA
のメンバーのディクショナリのキーに対するものであるため、約2倍の速度が期待できます。pickle
説明は、おそらく非常に似ています。