Ich arbeitete an einer einfachen Klasse , die erweitert dict
, und ich erkannte , dass die Schlüsselsuche und Verwendung pickle
sind sehr langsam.
Ich dachte, es sei ein Problem mit meiner Klasse, also habe ich einige triviale Benchmarks durchgeführt:
(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
Die Ergebnisse sind wirklich eine Überraschung. Während die Schlüsselsuche 2x langsamer ist, pickle
ist sie 5x langsamer.
Wie kann das sein? Andere Verfahren, wie get()
, __eq__()
und __init__()
, und Iteration über keys()
, values()
und items()
sind so schnell wie dict
.
EDIT : Ich habe mir den Quellcode von Python 3.9 angesehen und Objects/dictobject.c
es scheint, dass die __getitem__()
Methode von implementiert wird dict_subscript()
. Und dict_subscript()
verlangsamt Unterklassen nur, wenn der Schlüssel fehlt, da die Unterklasse implementiert werden kann __missing__()
und versucht, festzustellen, ob er vorhanden ist. Der Benchmark war jedoch ein vorhandener Schlüssel.
Aber mir ist etwas aufgefallen: __getitem__()
wird mit der Flagge definiert METH_COEXIST
. Und auch __contains__()
die andere Methode, die 2x langsamer ist, hat das gleiche Flag. Aus der offiziellen Dokumentation :
Die Methode wird anstelle der vorhandenen Definitionen geladen. Ohne METH_COEXIST werden standardmäßig wiederholte Definitionen übersprungen. Da Schlitz Umhüllungen vor dem Methodentabelle geladen werden, erzeugen würde die Existenz eines sq_contains Schlitz, beispielsweise eine umhüllten Methode namens enthält () , und schließt das Laden eines entsprechendes PyCFunction mit dem gleichen Namen. Wenn das Flag definiert ist, wird die PyCFunction anstelle des Wrapper-Objekts geladen und existiert neben dem Slot. Dies ist hilfreich, da Aufrufe von PyCFunctions mehr optimiert sind als Wrapper-Objektaufrufe.
Wenn ich es richtig verstanden habe, METH_COEXIST
sollte es theoretisch die Dinge beschleunigen, aber es scheint den gegenteiligen Effekt zu haben. Warum?
EDIT 2 : Ich habe etwas mehr entdeckt.
__getitem__()
und __contains()__
als Liste steht METH_COEXIST
, weil sie in PyDict_Type deklariert sind zwei mal.
Sie sind beide einmal im Slot vorhanden tp_methods
, wo sie explizit als __getitem__()
und deklariert werden __contains()__
. Aber die offizielle Dokumentation sagt , dass tp_methods
sind nicht von den Unterklassen geerbt.
Eine Unterklasse von dict
ruft also nicht auf __getitem__()
, sondern ruft den Unterschlitz auf mp_subscript
. In der Tat mp_subscript
ist in dem Slot enthalten tp_as_mapping
, der es einer Unterklasse ermöglicht, ihre Subslots zu erben.
Das Problem ist, dass beide __getitem__()
und mp_subscript
die gleiche Funktion verwenden dict_subscript
. Ist es möglich, dass nur die Art und Weise, wie es geerbt wurde, es verlangsamt?
len()
ist zum Beispiel nicht 2x langsamer, sondern hat die gleiche Geschwindigkeit?
len
sollte einen schnellen Weg für eingebaute Sequenztypen haben. Ich glaube nicht, dass ich in der Lage bin, Ihre Frage richtig zu beantworten, aber es ist eine gute. Hoffentlich wird jemand, der sich mit Python-Interna besser auskennt als ich, sie beantworten.
__contains__
Implementierung blockiert die zum Erben verwendete Logik sq_contains
.
dict
und in diesem Fall die C-Implementierung direkt aufruft, anstatt die__getitem__
Methode von nachzuschlagen die Klasse des Objekts. Ihr Code führt daher zwei Diktatsuchen durch, die erste für den Schlüssel'__getitem__'
im Wörterbuch der KlassenmitgliederA
, sodass davon ausgegangen werden kann, dass er etwa doppelt so langsam ist. Diepickle
Erklärung ist wahrscheinlich ziemlich ähnlich.