Я працював на простий клас , який простягається 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
.
EDIT : Я подивився вихідний код 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
слід прискорити справи, але, здається, це має зворотний ефект. Чому?
EDIT 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
Пояснення, ймовірно , дуже схожі.