Je travaillais sur une classe simple qui s'étend dict, et j'ai réalisé que la recherche de clés et l'utilisation de picklesont très lentes.
Je pensais que c'était un problème avec ma classe, alors j'ai fait quelques repères triviaux:
(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
Les résultats sont vraiment une surprise. Alors que la recherche de clé est 2 fois plus lente, elle pickleest 5 fois plus lente.
Comment se peut-il? D' autres méthodes, comme get(), __eq__()et __init__(), et l' itération sur keys(), values()et items()sont aussi rapides que dict.
EDIT : J'ai jeté un coup d'œil au code source de Python 3.9, et Objects/dictobject.cil semble que la __getitem__()méthode soit implémentée par dict_subscript(). Et ne dict_subscript()ralentit les sous-classes que si la clé est manquante, car la sous-classe peut être implémentée __missing__()et elle essaie de voir si elle existe. Mais la référence était avec une clé existante.
Mais j'ai remarqué quelque chose: __getitem__()est défini avec le drapeau METH_COEXIST. Et aussi __contains__(), l'autre méthode qui est 2x plus lente, a le même indicateur. De la documentation officielle :
La méthode sera chargée à la place des définitions existantes. Sans METH_COEXIST, la valeur par défaut est d'ignorer les définitions répétées. Étant donné que les wrappers d'emplacement sont chargés avant la table de méthodes, l'existence d'un emplacement sq_contains, par exemple, générerait une méthode encapsulée nommée contains () et empêcherait le chargement d'une fonction PyCFunction correspondante portant le même nom. Avec l'indicateur défini, la fonction PyCFunction sera chargée à la place de l'objet wrapper et coexistera avec l'emplacement. Cela est utile car les appels à PyCFunctions sont plus optimisés que les appels d'objet wrapper.
Donc, si j'ai bien compris, en théorie, cela METH_COEXISTdevrait accélérer les choses, mais cela semble avoir l'effet inverse. Pourquoi?
EDIT 2 : J'ai découvert quelque chose de plus.
__getitem__()et __contains()__sont marqués comme METH_COEXIST, car ils sont déclarés deux fois dans PyDict_Type .
Ils sont tous deux présents, une fois, dans la fente tp_methods, où ils sont explicitement déclarés comme __getitem__()et __contains()__. Mais la documentation officielle dit que netp_methods sont pas hérités par les sous-classes.
Ainsi, une sous-classe de dictn'appelle pas __getitem__(), mais appelle le sous-intervalle mp_subscript. En effet, mp_subscriptest contenu dans le slot tp_as_mapping, qui permet à une sous-classe d'hériter de ses sous-slots.
Le problème est que les deux __getitem__()et mp_subscriptutilisent la même fonction dict_subscript,. Est-il possible que ce soit seulement la façon dont il a été hérité qui le ralentisse?
len(), par exemple, n'est pas 2x plus lent mais a la même vitesse?
lendevrait y avoir un chemin rapide pour les types de séquence intégrés. Je ne pense pas être en mesure de donner une bonne réponse à votre question, mais elle est bonne, alors j'espère que quelqu'un qui connaît mieux les internes de Python que moi y répondra.
__contains__implémentation explicite bloque la logique utilisée pour l'héritage sq_contains.
dictet si oui, appelle directement l'implémentation C au lieu de rechercher la__getitem__méthode à partir de la classe de l'objet. Votre code effectue donc deux recherches de dict, la première pour la clé'__getitem__'dans le dictionnaire desAmembres de la classe , donc on peut s'attendre à ce qu'elle soit environ deux fois plus lente. L'pickleexplication est probablement assez similaire.