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?
sumber
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.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 mewarisisq_contains
.Jawaban:
Pengindeksan dan
in
lebih lambat dalamdict
subclass karena interaksi yang buruk antaradict
optimasi dan subclass logika digunakan untuk mewarisi slot C. Ini harus diperbaiki, meskipun bukan dari ujung Anda.Implementasi CPython memiliki dua set kait untuk kelebihan operator. Ada metode tingkat Python seperti
__contains__
dan__getitem__
, tetapi ada juga satu set slot yang terpisah untuk pointer fungsi C dalam tata letak memori objek tipe. Biasanya, metode Python akan menjadi pembungkus implementasi C, atau slot C akan berisi fungsi yang mencari dan memanggil metode Python. Ini lebih efisien untuk slot C untuk mengimplementasikan operasi secara langsung, karena slot C adalah apa yang sebenarnya diakses oleh Python.Pemetaan yang ditulis dalam C mengimplementasikan slot C
sq_contains
danmp_subscript
untuk menyediakanin
dan mengindeks. Biasanya, Python tingkat__contains__
dan__getitem__
metode akan secara otomatis dihasilkan sebagai bungkus sekitar fungsi C, tapidict
kelas memiliki implementasi eksplisit dari__contains__
dan__getitem__
, karena implementasi eksplisit sedikit lebih cepat dari pembungkus yang dihasilkan:(Sebenarnya,
__getitem__
implementasi eksplisit adalah fungsi yang sama denganmp_subscript
implementasi, hanya dengan jenis pembungkus yang berbeda.)Biasanya, sebuah subclass akan mewarisi implementasi induknya dari kait tingkat-C seperti
sq_contains
danmp_subscript
, dan subclass itu akan sama cepatnya dengan superclass. Namun, logika dalamupdate_one_slot
mencari implementasi induk dengan mencoba menemukan metode pembungkus yang dihasilkan melalui pencarian MRO.dict
tidak memiliki pembungkus dihasilkan untuksq_contains
danmp_subscript
, karena memberikan eksplisit__contains__
dan__getitem__
implementasi.Alih-alih mewarisi
sq_contains
danmp_subscript
,update_one_slot
akhirnya memberikan subclasssq_contains
danmp_subscript
implementasi yang melakukan pencarian MRO untuk__contains__
dan__getitem__
dan menyebutnya. Ini jauh lebih efisien daripada mewarisi slot C secara langsung.Memperbaiki ini akan membutuhkan perubahan pada
update_one_slot
implementasi.Selain dari apa yang saya jelaskan di atas,
dict_subscript
juga mencari__missing__
subclass dict, sehingga memperbaiki masalah pewarisan slot tidak akan membuat subclass sepenuhnya setara dengandict
dirinya sendiri untuk kecepatan pencarian, tetapi harus membuat mereka lebih dekat.Adapun acar, di
dumps
samping, implementasi acar memiliki jalur cepat khusus untuk dicts, sedangkan subclass dict mengambil jalur yang lebih bundaran melaluiobject.__reduce_ex__
dansave_reduce
.Di
loads
samping, perbedaan waktu sebagian besar hanya dari opcodes tambahan dan pencarian untuk mengambil dan membuat instance__main__.A
kelas, sementara dikt memiliki opcode acar khusus untuk membuat dikt baru. Jika kita membandingkan pembongkaran untuk acar:kita melihat bahwa perbedaan antara keduanya adalah bahwa acar kedua membutuhkan sejumlah besar opcodes untuk mencari
__main__.A
dan membuat instance, sedangkan acar pertama hanyaEMPTY_DICT
untuk mendapatkan dict kosong. Setelah itu, kedua acar mendorong kunci dan nilai yang sama ke tumpukan acar dan berlariSETITEMS
.sumber
__contains__()
dan__getitem()
sedemikian rupa sehingga dapat diwarisi oleh subclass? Dalam dokumentasi resmitp_methods
, tertulis itumethods are inherited through a different mechanism
, jadi sepertinya mungkin.__contains__
dan__getitem__
yang diwariskan, tetapi masalahnya adalah bahwasq_contains
danmp_subscript
tidak.__contains__
dan__getitem__
berada dalam slottp_methods
, bahwa untuk dokumen resmi tidak diwarisi oleh subclass. Dan seperti yang Anda katakan,update_one_slot
tidak menggunakansq_contains
danmp_subscript
.contains
dan sisanya tidak bisa hanya dipindahkan di slot lain, yang diwarisi oleh subclass?tp_methods
tidak diwariskan, tetapi objek metode Python yang dihasilkan darinya diwarisi dalam arti bahwa pencarian MRO standar untuk akses atribut akan menemukannya.