Pracowałem nad prostą klasą, która rozszerza się dict
i zdałem sobie sprawę, że wyszukiwanie klucza i korzystanie z nich pickle
jest bardzo wolne.
Myślałem, że to problem z moją klasą, więc zrobiłem kilka prostych testów:
(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
Wyniki są naprawdę niespodzianką. Podczas gdy wyszukiwanie klucza jest 2x wolniejsze, pickle
jest pięciokrotnie wolniejsze.
Jak to może być? Inne metody, takie jak get()
, __eq__()
i __init__()
, i iteracja nad keys()
, values()
i items()
to jak najszybciej dict
.
EDYCJA : Rzuciłem okiem na kod źródłowy Pythona 3.9 i Objects/dictobject.c
wydaje się, że __getitem__()
metoda jest zaimplementowana przez dict_subscript()
. I dict_subscript()
spowalnia podklasy tylko wtedy, gdy brakuje klucza, ponieważ podklasa może się zaimplementować __missing__()
i próbuje sprawdzić, czy istnieje. Ale punktem odniesienia był istniejący klucz.
Ale zauważyłem coś: __getitem__()
jest zdefiniowane za pomocą flagi METH_COEXIST
. Również __contains__()
druga metoda, która jest 2x wolniejsza, ma tę samą flagę. Z oficjalnej dokumentacji :
Metoda zostanie załadowana w miejsce istniejących definicji. Bez METH_COEXIST domyślnie pomija się powtarzające się definicje. Ponieważ opakowania szczelin są ładowane przed tabelą metod, istnienie gniazda sq_contains zawiera na przykład wygenerowanie zawiniętej metody o nazwie zawiera () i wyklucza ładowanie odpowiedniej funkcji PyC o tej samej nazwie. Po zdefiniowaniu flagi funkcja PyCFunkcja zostanie załadowana w miejsce obiektu opakowania i będzie współistnieć z gniazdem. Jest to pomocne, ponieważ wywołania funkcji PyC są zoptymalizowane bardziej niż wywołania obiektu opakowania.
Więc jeśli dobrze zrozumiałem, teoretycznie METH_COEXIST
powinno to przyspieszyć, ale wydaje się, że ma to odwrotny skutek. Dlaczego?
EDYCJA 2 : Odkryłem coś więcej.
__getitem__()
i __contains()__
są oznaczane jako METH_COEXIST
, ponieważ są zadeklarowane w PyDict_Type dwa razy.
Obaj są obecni jednorazowo w automacie tp_methods
, gdzie są wyraźnie zadeklarowani jako __getitem__()
i __contains()__
. Ale oficjalna dokumentacja mówi, że nietp_methods
są dziedziczone przez podklasy.
Tak więc podklasa dict
nie wywołuje __getitem__()
, ale wywołuje sublot mp_subscript
. Rzeczywiście, mp_subscript
jest zawarty w gnieździe tp_as_mapping
, który pozwala podklasie odziedziczyć podsieci.
Problem polega na tym, że zarówno __getitem__()
i mp_subscript
użyć tej samej funkcji dict_subscript
. Czy to możliwe, że spowalnia to tylko sposób odziedziczenia?
len()
na przykład nie jest 2x wolniejszy, ale ma taką samą prędkość?
len
powinienem mieć szybką ścieżkę dla wbudowanych typów sekwencji. Nie sądzę, że jestem w stanie udzielić właściwej odpowiedzi na twoje pytanie, ale jest to dobre, więc mam nadzieję, że ktoś bardziej kompetentny na temat wewnętrznych elementów Pythona ode mnie odpowie.
__contains__
implementacja blokuje logikę używaną do dziedziczenia sq_contains
.
dict
a jeśli tak, wywołuje implementację C bezpośrednio zamiast szukać__getitem__
metody z klasa obiektu. Dlatego twój kod wykonuje dwa wyszukiwania odnośników, pierwszy dla klucza'__getitem__'
w słowniku członków klasyA
, więc można oczekiwać, że będzie około dwa razy wolniejszy.pickle
Wyjaśnienie jest chyba dość podobny.