Я работал на простой класс , который простирается 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Объяснение, вероятно , очень похожи.