나는 확장하는 간단한 클래스에서 일하고 있었고 dict
키 조회 및 사용 pickle
이 매우 느리다 는 것을 깨달았습니다 .
수업에 문제가 있다고 생각했기 때문에 몇 가지 간단한 벤치 마크를 수행했습니다.
(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
결과는 정말 놀랍습니다. 키 조회는 2 배 느리지 pickle
만 5 배 느립니다.
어떻게 이럴 수있어? 다른 방법처럼 get()
, __eq__()
그리고 __init__()
, 이상 반복 keys()
, values()
그리고 items()
최대한 빨리이다 dict
.
편집 : 파이썬 3.9의 소스 코드를 살펴 보았고 메소드가에 의해 구현 된 Objects/dictobject.c
것 같습니다 . 그리고 서브 클래스가 구현할 수 있기 때문에 서브 클래스 느려지는 키가없는 경우에만 하고 그것이 존재하는지 확인하려고합니다. 그러나 벤치 마크는 기존 키를 사용했습니다.__getitem__()
dict_subscript()
dict_subscript()
__missing__()
그러나 나는 무언가를 알아 냈습니다 : __getitem__()
플래그로 정의되었습니다 METH_COEXIST
. 또한 __contains__()
2 배 느린 다른 방법은 동일한 플래그를 갖습니다. 로부터 공식 문서 :
기존 정의 대신 메소드가로드됩니다. METH_COEXIST가 없으면 기본값은 반복되는 정의를 건너 뛰는 것입니다. 슬롯 랩퍼는 메소드 테이블 전에로드되므로 sq_contains 슬롯의 존재는 예를 들어 contains () 라는 랩핑 된 메소드를 생성 하고 동일한 이름의 해당 PyCFunction을로드하지 못하게합니다. 플래그가 정의되면 PyCFunction이 랩퍼 오브젝트 대신로드되고 슬롯과 공존합니다. 이는 PyCFunction에 대한 호출이 랩퍼 오브젝트 호출보다 최적화되어 있기 때문에 유용합니다.
따라서 내가 올바르게 이해하면 이론상으로 METH_COEXIST
속도를 높여야하지만 반대 효과가있는 것 같습니다. 왜?
편집 2 : 뭔가 더 발견했습니다.
__getitem__()
와 __contains()__
같은 플래그가 METH_COEXIST
그들이 PyDict_Type에 선언되어 있기 때문에, 두 번.
둘 다 슬롯에 한 번 존재하며 tp_methods
명시 적으로 __getitem__()
and로 선언됩니다 __contains()__
. 그러나 공식 문서는 말한다 tp_methods
되어 있지 서브 클래스에 의해 상속.
따라서의 서브 클래스는 dict
호출하지 않지만 __getitem__()
서브 슬롯을 호출합니다 mp_subscript
. 실제로 mp_subscript
slot에 포함되어 tp_as_mapping
서브 클래스가 서브 슬롯을 상속 할 수 있도록합니다.
문제는 둘 것입니다 __getitem__()
및 mp_subscript
사용 같은 , 기능 dict_subscript
. 그것이 상속받은 방식으로 만 느려지는 것이 가능합니까?
len()
예를 들어 왜 2 배는 느리지 않지만 같은 속도를 가지는가 ?
len
내장 시퀀스 유형에 대한 빠른 경로 가 있어야한다고 생각했을 것 입니다. 나는 당신의 질문에 대한 올바른 대답을 줄 수 없다고 생각하지만, 좋은 질문이므로 파이썬 내부에 대해 더 많은 지식을 가진 사람이 대답 할 것입니다.
__contains__
구현이 상속에 사용되는 논리를 차단하고 sq_contains
있습니다.
dict
그렇다면 C 구현을 호출하는 대신 직접의 찾는__getitem__
에서 방법을 객체의 클래스 따라서 코드'__getitem__'
는 클래스A
멤버 사전의 키 에 대한 첫 번째 dict 조회 를 두 번 수행하므로 약 두 배 느릴 것으로 예상됩니다.pickle
설명은 아마 매우 유사하다.