Saya bekerja pada sebuah kelas sederhana yang meluas dict
, dan saya menyadari bahwa kunci pencarian dan penggunaan pickle
yang sangat lambat.
Saya pikir itu masalah dengan kelas saya, jadi saya melakukan beberapa tolok ukur sepele:
(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
Hasilnya sungguh mengejutkan. Sementara pencarian kunci 2x lebih lambat, pickle
adalah 5x lebih lambat.
Bagaimana ini bisa terjadi? Metode lain, seperti get()
, __eq__()
dan __init__()
, dan iterasi berakhir keys()
, values()
dan items()
secepat dict
.
EDIT : Saya melihat kode sumber Python 3.9, dan Objects/dictobject.c
tampaknya __getitem__()
metode ini diimplementasikan oleh dict_subscript()
. Dan dict_subscript()
memperlambat subclass hanya jika kuncinya hilang, karena subclass dapat diimplementasikan __missing__()
dan mencoba untuk melihat apakah ada. Namun tolok ukurnya adalah dengan kunci yang ada.
Tapi saya perhatikan sesuatu: __getitem__()
didefinisikan dengan bendera METH_COEXIST
. Dan juga __contains__()
, metode lain yaitu 2x lebih lambat, memiliki flag yang sama. Dari dokumentasi resmi :
Metode ini akan dimuat di tempat definisi yang ada. Tanpa METH_COEXIST, standarnya adalah melompati definisi yang berulang. Karena pembungkus slot dimuat sebelum tabel metode, keberadaan slot sq_contains, misalnya, akan menghasilkan metode terbungkus bernama berisi () dan mencegah pemuatan fungsi PyCF yang sesuai dengan nama yang sama. Dengan flag yang ditentukan, PyCFunction akan dimuat di tempat objek wrapper dan akan hidup berdampingan dengan slot. Ini membantu karena panggilan ke PyCFunctions dioptimalkan lebih dari panggilan objek wrapper.
Jadi jika saya mengerti dengan benar, secara teori METH_COEXIST
harus mempercepat, tetapi tampaknya memiliki efek sebaliknya. Mengapa?
EDIT 2 : Saya menemukan sesuatu yang lebih.
__getitem__()
dan __contains()__
ditandai sebagai METH_COEXIST
, karena dideklarasikan dalam PyDict_Type dua kali.
Keduanya hadir, satu kali, di slot tp_methods
, di mana mereka secara eksplisit dinyatakan sebagai __getitem__()
dan __contains()__
. Tetapi dokumentasi resmi mengatakan bahwa tp_methods
itu tidak diwarisi oleh subclass.
Jadi subkelas dict
tidak memanggil __getitem__()
, tetapi memanggil subslot mp_subscript
. Memang, mp_subscript
terkandung dalam slot tp_as_mapping
, yang memungkinkan subclass untuk mewarisi sublotnya.
Masalahnya adalah bahwa keduanya __getitem__()
dan mp_subscript
menggunakan fungsi yang samadict_subscript
,. Apakah mungkin hanya warisan yang memperlambatnya?
len()
, misalnya, tidak lebih lambat 2x tetapi memiliki kecepatan yang sama?
len
harus memiliki jalur cepat untuk tipe urutan bawaan. Saya tidak berpikir saya bisa memberikan jawaban yang tepat untuk pertanyaan Anda, tetapi itu adalah jawaban yang bagus, jadi semoga seseorang yang lebih berpengetahuan tentang internal Python daripada saya akan menjawabnya.
__contains__
Implementasi eksplisit memblokir logika yang digunakan untuk mewarisi sq_contains
.
dict
dan jika demikian, panggil implementasi C secara langsung alih-alih mencari__getitem__
metode dari kelas objek. Karenanya kode Anda melakukan dua pencarian dict, yang pertama untuk kunci'__getitem__'
dalam kamus anggota kelasA
, sehingga dapat diperkirakan sekitar dua kali lebih lambat. Thepickle
penjelasan mungkin cukup mirip.