Estaba trabajando en una clase simple que se extiende dicty me di cuenta de que la búsqueda y el uso de claves pickleson 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.611666693352163Los resultados son realmente una sorpresa. Mientras que la búsqueda de claves es 2 veces más lenta, picklees 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.cparece 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_COEXISTdeberí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 dictno llama __getitem__(), sino que llama al subslot mp_subscript. De hecho, mp_subscriptestá contenido en la ranura tp_as_mapping, que permite que una subclase herede sus sublotas.
El problema es que ambos __getitem__()y mp_subscriptusan la misma función dict_subscript,. ¿Es posible que sea solo la forma en que se heredó lo que lo ralentiza?
fuente

dictasí, 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 deAlos miembros de la clase , por lo que se puede esperar que sea aproximadamente el doble de lenta. Lapickleexplicación es probablemente bastante similar.len()por qué , por ejemplo, no es 2 veces más lento pero tiene la misma velocidad?lendeberí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 heredarsq_contains.Respuestas:
La indexación y
inlasdictsubclases son más lentas debido a una mala interacción entre unadictoptimización y las subclases lógicas utilizadas para heredar las ranuras C. Esto debería ser reparable, aunque no de su parte.La implementación de CPython tiene dos conjuntos de ganchos para sobrecargas del operador. Existen métodos a nivel de Python como
__contains__y__getitem__, pero también hay un conjunto separado de ranuras para punteros de función C en el diseño de memoria de un objeto tipo. Por lo general, el método Python será un envoltorio alrededor de la implementación de C o la ranura C contendrá una función que busca y llama al método Python. Es más eficiente que la ranura C implemente la operación directamente, ya que la ranura C es a lo que Python realmente accede.Las asignaciones escritas en C implementan las ranuras C
sq_containsymp_subscriptproporcionanine indexan. Normalmente, el nivel de Python__contains__y los__getitem__métodos se generarían automáticamente como envoltorios alrededor de las funciones de C, pero ladictclase tiene implementaciones explícitas de__contains__y__getitem__, porque las implementaciones explícitas son un poco más rápidas que los envoltorios generados:(En realidad, la
__getitem__implementación explícita es la misma función que lamp_subscriptimplementación, solo que con un tipo diferente de envoltorio).Por lo general, una subclase heredaría las implementaciones de sus padres de ganchos de nivel C como
sq_containsymp_subscript, y la subclase sería tan rápida como la superclase. Sin embargo, la lógicaupdate_one_slotbusca la implementación principal al tratar de encontrar los métodos de contenedor generados a través de una búsqueda MRO.dictno tienen envolturas generados parasq_containsymp_subscript, debido a que proporciona explícita__contains__e__getitem__implementaciones.En lugar de heredar
sq_containsymp_subscript,update_one_slottermina dando la subclasesq_containsymp_subscriptlas implementaciones que realizar una búsqueda de MRO__contains__y__getitem__y llamar a aquellos. Esto es mucho menos eficiente que heredar las ranuras C directamente.Arreglar esto requerirá cambios en la
update_one_slotimplementación.Además de lo que describí anteriormente,
dict_subscripttambién busca__missing__subclases dict, por lo que solucionar el problema de herencia de ranura no hará que las subclases estén completamente a la pardictpara la velocidad de búsqueda, pero debería acercarlas mucho más.En cuanto al encurtido, por un
dumpslado, la implementación de encurtido tiene una ruta rápida dedicada para los dictados, mientras que la subclase dict toma un camino más indirecto a través deobject.__reduce_ex__ysave_reduce.Por
loadsotro lado, la diferencia horaria se debe principalmente a los__main__.Acódigos de operación y las búsquedas adicionales para recuperar e instanciar la clase, mientras que los dictos tienen un código de operación de pickle dedicado para hacer un nuevo dict. Si comparamos el desmontaje de los encurtidos:Vemos que la diferencia entre los dos es que el segundo encurtido necesita un montón de códigos de operación para mirar hacia arriba
__main__.Ae instanciarlo, mientras que el primer encurtido solo lo haceEMPTY_DICTpara obtener un dict vacío. Después de eso, ambos pepinillos empujan las mismas teclas y valores en la pila de operandos de pepinillos y se ejecutanSETITEMS.fuente
__contains__()y__getitem()de tal manera que pueda ser heredada por las subclases? En la documentación oficial detp_methods, está escrito esomethods are inherited through a different mechanism, por lo que parece posible.__contains__y__getitem__se heredan, pero el problema es esosq_containsymp_subscriptno lo son.__contains__y__getitem__están en la ranuratp_methods, que para los documentos oficiales no son heredados por las subclases. Y como dijiste,update_one_slotno usasq_containsymp_subscript.containspocas palabras, y el resto no se puede mover simplemente a otra ranura, que es heredada por las subclases.tp_methodsno se hereda, pero los objetos del método Python generados a partir de él se heredan en el sentido de que la búsqueda estándar de MRO para acceso a atributos los encontrará.