Я работал на простой класс , который простирается 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
.
РЕДАКТИРОВАТЬ : я взглянул на исходный код Python 3.9, и, Objects/dictobject.c
кажется, что __getitem__()
метод реализован dict_subscript()
. И dict_subscript()
замедляет подклассы, только если ключ отсутствует, так как подкласс может реализовать, __missing__()
и он пытается видеть, существует ли он. Но тест был с существующим ключом.
Но я кое-что заметил: __getitem__()
определяется флагом METH_COEXIST
. А также __contains__()
, другой метод, который в 2 раза медленнее, имеет тот же флаг. Из официальной документации :
Метод будет загружен вместо существующих определений. Без METH_COEXIST по умолчанию пропускаются повторные определения. Так как слот обертка загружается перед таблицей методы, наличие слота sq_contains, например, будет генерировать завернутый метод с именем содержит () и исключает загрузку соответствующего PyCFunction с тем же именем. С установленным флагом PyCFunction будет загружен вместо объекта-оболочки и будет сосуществовать со слотом. Это полезно, потому что вызовы PyCFunctions оптимизированы больше, чем вызовы объектов-оболочек.
Так что, если я правильно понял, теоретически METH_COEXIST
следует ускорить процесс, но, похоже, это имеет противоположный эффект. Почему?
РЕДАКТИРОВАТЬ 2 : я обнаружил нечто большее.
__getitem__()
и __contains()__
помечены как METH_COEXIST
, потому что они объявлены в PyDict_Type два раза.
Они оба присутствуют, один раз, в слоте tp_methods
, где они явно объявлены как __getitem__()
и __contains()__
. Но официальная документация говорит , что tp_methods
являются не наследуются подклассами.
Таким образом, подкласс dict
не вызывает __getitem__()
, но вызывает подслот mp_subscript
. Действительно, mp_subscript
содержится в слоте tp_as_mapping
, что позволяет подклассу наследовать его подслоты.
Проблема в том, что оба __getitem__()
и mp_subscript
используют одну и ту же функцию dict_subscript
. Возможно ли, что только способ наследования замедляет его?
len()
, например, не в 2 раза медленнее, а с такой же скоростью?
len
должен иметь быстрый путь для встроенных типов последовательности. Я не думаю, что смогу дать правильный ответ на ваш вопрос, но это хороший вопрос, так что, надеюсь, на него ответит кто-то более осведомленный о внутренностях Python, чем я.
__contains__
реализация блокирует логику, используемую для наследования sq_contains
.
dict
и, если это так, вызывает реализацию C напрямую, вместо того, чтобы искать__getitem__
метод из класс объекта. Таким образом, ваш код выполняет два точных поиска, первый - для ключа'__getitem__'
в словаре членов классаA
, поэтому можно ожидать, что он будет примерно в два раза медленнее.pickle
Объяснение, вероятно , очень похожи.