Saya bekerja pada sebuah kelas sederhana yang meluas dict, dan saya menyadari bahwa kunci pencarian dan penggunaan pickleyang 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, pickleadalah 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.ctampaknya __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_COEXISTharus 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_methodsitu tidak diwarisi oleh subclass.
Jadi subkelas dicttidak memanggil __getitem__(), tetapi memanggil subslot mp_subscript. Memang, mp_subscriptterkandung dalam slot tp_as_mapping, yang memungkinkan subclass untuk mewarisi sublotnya.
Masalahnya adalah bahwa keduanya __getitem__()dan mp_subscriptmenggunakan fungsi yang samadict_subscript ,. Apakah mungkin hanya warisan yang memperlambatnya?
len(), misalnya, tidak lebih lambat 2x tetapi memiliki kecepatan yang sama?
lenharus 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.
dictdan 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. Thepicklepenjelasan mungkin cukup mirip.