Stavo lavorando su una semplice classe che si estende dict
e mi sono reso conto che la ricerca e l'uso dei tasti pickle
sono molto lenti.
Ho pensato che fosse un problema con la mia classe, quindi ho fatto alcuni banali benchmark:
(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
I risultati sono davvero una sorpresa. Mentre la ricerca dei tasti è 2 volte più lenta, pickle
è 5 volte più lenta.
Come può essere? Altri metodi, come get()
, __eq__()
e __init__()
, e iterazione keys()
, values()
e items()
sono veloci come dict
.
EDIT : ho dato un'occhiata al codice sorgente di Python 3.9, e Objects/dictobject.c
sembra che il __getitem__()
metodo sia implementato da dict_subscript()
. E dict_subscript()
rallenta le sottoclassi solo se manca la chiave, poiché la sottoclasse può implementare __missing__()
e prova a vedere se esiste. Ma il benchmark era con una chiave esistente.
Ma ho notato qualcosa: __getitem__()
è definito con la bandiera METH_COEXIST
. Inoltre __contains__()
, l'altro metodo che è 2x più lento, ha lo stesso flag. Dalla documentazione ufficiale :
Il metodo verrà caricato al posto delle definizioni esistenti. Senza METH_COEXIST, l'impostazione predefinita è saltare definizioni ripetute. Dato che i wrapper di slot vengono caricati prima della tabella dei metodi, l'esistenza di uno slot sq_contains, ad esempio, genererebbe un metodo di wrapping chiamato contiene () e precluderebbe il caricamento di una corrispondente PyCFunction con lo stesso nome. Con il flag definito, PyCFunction verrà caricato al posto dell'oggetto wrapper e coesisterà con lo slot. Questo è utile perché le chiamate a PyCFunctions sono ottimizzate più delle chiamate di oggetti wrapper.
Quindi, se ho capito bene, in teoria METH_COEXIST
dovrebbe accelerare le cose, ma sembra avere l'effetto opposto. Perché?
EDIT 2 : ho scoperto qualcosa di più.
__getitem__()
e __contains()__
sono contrassegnati come METH_COEXIST
, perché sono dichiarati in PyDict_Type due volte.
Sono entrambi presenti, una volta, nello slot tp_methods
, dove sono esplicitamente dichiarati come __getitem__()
e __contains()__
. Ma la documentazione ufficiale afferma che nontp_methods
sono ereditati da sottoclassi.
Quindi una sottoclasse di dict
non chiama __getitem__()
, ma chiama la sottopartita mp_subscript
. In effetti, mp_subscript
è contenuto nello slot tp_as_mapping
, che consente a una sottoclasse di ereditare i suoi sottogruppi.
Il problema è che sia __getitem__()
e mp_subscript
utilizzano la stessa funzione, dict_subscript
. È possibile che sia solo il modo in cui è stato ereditato a rallentarlo?
len()
, ad esempio, non è 2x più lento ma ha la stessa velocità?
len
dovrebbe avere un percorso veloce per i tipi di sequenza incorporati. Non credo di essere in grado di dare una risposta adeguata alla tua domanda, ma è una buona risposta, quindi spero che qualcuno più esperto di Python internals di me risponderà.
__contains__
implementazione esplicita sta bloccando la logica utilizzata per ereditare sq_contains
.
dict
e, in tal caso, chiama l'implementazione C direttamente invece di cercare il__getitem__
metodo da la classe dell'oggetto. Pertanto, il tuo codice esegue due ricerche dict, la prima per la chiave'__getitem__'
nel dizionario deiA
membri della classe , quindi ci si può aspettare che sia circa due volte più lento. Lapickle
spiegazione è probabilmente abbastanza simile.