Eu estava trabalhando em uma classe simples que se estende dict
e percebi que a pesquisa e o uso de chaves pickle
são muito lentos.
Eu pensei que era um problema com a minha classe, então fiz alguns benchmarks triviais:
(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
Os resultados são realmente uma surpresa. Enquanto a pesquisa de teclas é 2x mais lenta, pickle
é 5x mais lenta.
Como isso pode ser? Outros métodos, como get()
, __eq__()
e __init__()
, e iteração terminam keys()
, values()
e items()
são tão rápidos quanto dict
.
Edição : dei uma olhada no código fonte do Python 3.9, e Objects/dictobject.c
parece que o __getitem__()
método é implementado por dict_subscript()
. E dict_subscript()
diminui a velocidade das subclasses apenas se a chave estiver faltando, pois a subclasse pode ser implementada __missing__()
e tenta ver se existe. Mas a referência foi com uma chave existente.
Mas notei algo: __getitem__()
é definido com a bandeira METH_COEXIST
. E também __contains__()
, o outro método que é 2x mais lento, tem a mesma bandeira. A partir da documentação oficial :
O método será carregado no lugar das definições existentes. Sem METH_COEXIST, o padrão é pular definições repetidas. Como os wrappers de slot são carregados antes da tabela de métodos, a existência de um slot sq_contains, por exemplo, geraria um método empacotado chamado contains () e impediria o carregamento de uma PyCFunction correspondente com o mesmo nome. Com o sinalizador definido, o PyCFunction será carregado no lugar do objeto wrapper e coexistirá com o slot. Isso é útil porque as chamadas para PyCFunctions são otimizadas mais do que as chamadas de objeto de wrapper.
Então, se eu entendi direito, em teoria, METH_COEXIST
deveria acelerar as coisas, mas parece ter o efeito oposto. Por quê?
EDIT 2 : Eu descobri algo mais.
__getitem__()
e __contains()__
são sinalizados como METH_COEXIST
, porque são declarados em PyDict_Type duas vezes.
Ambos estão presentes, uma vez, no slot tp_methods
, onde são explicitamente declarados como __getitem__()
e __contains()__
. Mas a documentação oficial diz que nãotp_methods
são herdadas pelas subclasses.
Portanto, uma subclasse de dict
não chama __getitem__()
, mas chama o sub-lote mp_subscript
. De fato, mp_subscript
está contido no slot tp_as_mapping
, que permite que uma subclasse herde seus sub-lotes.
O problema é que ambos __getitem__()
e mp_subscript
usam a mesma função dict_subscript
,. É possível que seja apenas a maneira como foi herdada que a torna mais lenta?
len()
, por exemplo, não é 2x mais lento, mas tem a mesma velocidade?
len
deveria ter um caminho rápido para os tipos de sequência internos. Eu não acho que sou capaz de dar uma resposta adequada à sua pergunta, mas é uma boa, então espero que alguém com mais conhecimento sobre o interior do Python do que eu possa responder.
__contains__
implementação explícita está bloqueando a lógica usada para herdar sq_contains
.
dict
e, nesse caso, chama a implementação C diretamente, em vez de procurar o__getitem__
método em a classe do objeto. Seu código, portanto, faz duas pesquisas de ditado, a primeira para a chave'__getitem__'
no dicionário dosA
membros da classe , portanto, pode-se esperar que seja duas vezes mais lento. Apickle
explicação é provavelmente bastante semelhante.