Estaba trabajando en una clase simple que se extiende dict
y me di cuenta de que la búsqueda y el uso de claves pickle
son muy lentos.
Pensé que era un problema con mi clase, así que hice algunos puntos de referencia triviales:
(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
Los resultados son realmente una sorpresa. Mientras que la búsqueda de claves es 2 veces más lenta, pickle
es 5 veces más lenta.
¿Cómo puede ser esto? Otros métodos, como get()
, __eq__()
y __init__()
, e iteración keys()
, values()
y items()
son tan rápidos como dict
.
EDITAR : Eché un vistazo al código fuente de Python 3.9, y Objects/dictobject.c
parece que el __getitem__()
método está implementado por dict_subscript()
. Y dict_subscript()
ralentiza las subclases solo si falta la clave, ya que la subclase puede implementar __missing__()
e intenta ver si existe. Pero el punto de referencia fue con una clave existente.
Pero noté algo: __getitem__()
se define con la bandera METH_COEXIST
. Y también __contains__()
, el otro método que es 2 veces más lento, tiene la misma bandera. De la documentación oficial :
El método se cargará en lugar de las definiciones existentes. Sin METH_COEXIST, el valor predeterminado es omitir definiciones repetidas. Dado que los contenedores de ranuras se cargan antes de la tabla de métodos, la existencia de una ranura sq_contains, por ejemplo, generaría un método envuelto llamado contiene () y evitaría la carga de una PyCFunction correspondiente con el mismo nombre. Con la bandera definida, la función PyCFunction se cargará en lugar del objeto contenedor y coexistirá con la ranura. Esto es útil porque las llamadas a PyCFunctions se optimizan más que las llamadas a objetos de contenedor.
Entonces, si entendí correctamente, en teoría METH_COEXIST
debería acelerar las cosas, pero parece tener el efecto contrario. ¿Por qué?
EDIT 2 : descubrí algo más.
__getitem__()
y __contains()__
se marcan como METH_COEXIST
, porque se declaran en PyDict_Type dos veces.
Ambos están presentes, una vez, en la ranura tp_methods
, donde se declaran explícitamente como __getitem__()
y __contains()__
. Pero la documentación oficial dice que notp_methods
son heredadas por las subclases.
Entonces, una subclase de dict
no llama __getitem__()
, sino que llama al subslot mp_subscript
. De hecho, mp_subscript
está contenido en la ranura tp_as_mapping
, que permite que una subclase herede sus sublotas.
El problema es que ambos __getitem__()
y mp_subscript
usan la misma función dict_subscript
,. ¿Es posible que sea solo la forma en que se heredó lo que lo ralentiza?
len()
por qué , por ejemplo, no es 2 veces más lento pero tiene la misma velocidad?
len
debería tener una ruta rápida para los tipos de secuencia incorporados. No creo que pueda dar una respuesta adecuada a su pregunta, pero es buena, por lo que espero que alguien más conocedor de Python que yo responda.
__contains__
implementación explícita está bloqueando la lógica utilizada para heredar sq_contains
.
dict
así, llama a la implementación de C directamente en lugar de buscar el__getitem__
método desde La clase del objeto. Por lo tanto, su código realiza dos búsquedas de dict, la primera para la clave'__getitem__'
en el diccionario deA
los miembros de la clase , por lo que se puede esperar que sea aproximadamente el doble de lenta. Lapickle
explicación es probablemente bastante similar.