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?
źródło
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.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 dziedziczeniasq_contains
.Odpowiedzi:
Indeksowanie i
in
są wolniejsze wdict
podklasach z powodu złej interakcji międzydict
optymalizacją a logicznymi podklasami używanymi do dziedziczenia szczelin C. Powinno to zostać naprawione, ale nie od końca.Implementacja CPython ma dwa zestawy zaczepów dla przeciążeń operatora. Istnieją metody na poziomie Python, takie jak
__contains__
i__getitem__
, ale istnieje również osobny zestaw miejsc dla wskaźników funkcji C w układzie pamięci obiektu typu. Zwykle albo metoda Python będzie owijała się wokół implementacji języka C, lub gniazdo C będzie zawierało funkcję, która wyszukuje i wywołuje metodę języka Python. Bardziej wydajne jest, aby gniazdo C realizowało operację bezpośrednio, ponieważ gniazdo C jest tym, do którego faktycznie uzyskuje dostęp Python.Mapowania napisane w C implementują gniazda C
sq_contains
orazmp_subscript
udostępniająin
i indeksują. Zwykle Python poziomu__contains__
i__getitem__
metody będą automatycznie generowane jako obwolut wokół funkcji C, aledict
klasa ma wyraźne implementacje o__contains__
i__getitem__
, ponieważ jawne implementacje są nieco szybciej niż generowanych owijarki:(W rzeczywistości jawna
__getitem__
implementacja jest tą samą funkcją comp_subscript
implementacja, tylko z innym rodzajem opakowania).Zwykle podklasa dziedziczy implementacje rodzicielskie haków na poziomie C, takich jak
sq_contains
imp_subscript
, a podklasa byłaby równie szybka jak nadklasa. Jednak logikaupdate_one_slot
szuka implementacji nadrzędnej, próbując znaleźć wygenerowane metody opakowania za pomocą wyszukiwania MRO.dict
nie mają generowane obwoluty dlasq_contains
imp_subscript
, ponieważ zapewnia wyraźny__contains__
i__getitem__
implementacje.Zamiast dziedziczy
sq_contains
imp_subscript
,update_one_slot
kończy się dając podklasęsq_contains
imp_subscript
implementacje że przeprowadzenie MRO szukać__contains__
i__getitem__
i nazywają te. Jest to o wiele mniej wydajne niż bezpośrednie dziedziczenie slotów C.Naprawienie tego wymagać będzie zmian w
update_one_slot
implementacji.Oprócz tego, co opisałem powyżej,
dict_subscript
wyszukuje także__missing__
podklasy dict, więc naprawienie problemu dziedziczenia slotów nie spowoduje, że podklasy będą na równi zedict
sobą pod względem szybkości wyszukiwania, ale powinny zbliżyć je znacznie bardziej.Jeśli chodzi o wytrawianie, z
dumps
boku implementacja wytrawiania ma dedykowaną szybką ścieżkę dla nagrań, podczas gdy podklasa nagrań ma bardziej okrągłą ścieżkę przezobject.__reduce_ex__
isave_reduce
.Z
loads
boku różnica czasu wynika głównie z dodatkowych kodów operacyjnych i odnośników do wyszukiwania i tworzenia__main__.A
klasy, podczas gdy dykty mają dedykowany kod piklujący do tworzenia nowego dykta. Jeśli porównamy demontaż pikli:widzimy, że różnica między nimi polega na tym, że druga marynata potrzebuje całej gamy kodów do wyszukiwania
__main__.A
i tworzenia jej, podczas gdy pierwsza marynata robi tylkoEMPTY_DICT
pusty dyktand. Następnie oba pikle pchają te same klucze i wartości na stos operandu piklującego i biegnąSETITEMS
.źródło
__contains__()
i__getitem()
w taki sposób, że mogą być dziedziczone przez podklasy? W oficjalnej dokumentacjitp_methods
jest napisanemethods are inherited through a different mechanism
, że wydaje się to możliwe.__contains__
i__getitem__
są dziedziczone, ale problemem jest to, żesq_contains
imp_subscript
nie są.__contains__
i__getitem__
są w boksietp_methods
, że dla oficjalnych dokumentów nie są dziedziczone przez podklasy. I jak powiedziałeś,update_one_slot
nie używasq_contains
imp_subscript
.contains
Innymi słowy, a reszty nie można po prostu przenieść w inne miejsce, które jest dziedziczone przez podklasy?tp_methods
nie jest dziedziczony, ale obiekty metody Python wygenerowane z niego są dziedziczone w tym sensie, że standardowe wyszukiwanie MRO dostępu do atrybutów je znajdzie.