Ben uzanan basit bir sınıfın üzerinde çalışıyordu dict
ve ben o anahtar arama ve kullanımını fark pickle
vardır çok yavaş.
Sınıfımla ilgili bir sorun olduğunu düşündüm, bu yüzden bazı önemsiz ölçütler yaptım:
(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
Sonuçlar gerçekten şaşırtıcı. Tuş araması 2 kat daha yavaşkenpickle
, 5 kat daha yavaştır.
Bu nasıl olabilir? Diğer yöntemler gibi get()
, __eq__()
ve __init__()
, ve üzerinde yineleme keys()
, values()
ve items()
olabildiğince hızlı gibidir dict
.
DÜZENLEME : Python 3.9 kaynak koduna bir göz attım ve içinde yöntem tarafından uygulanmış Objects/dictobject.c
gibi görünüyor . Ve alt sınıf uygulayabilirsiniz beri alt sınıflara yavaşlar anahtar, eksik yalnızca ve varsa görmeye çalışır. Ancak kriter mevcut bir anahtarla yapıldı.__getitem__()
dict_subscript()
dict_subscript()
__missing__()
Ama bir şey fark ettim: __getitem__()
bayrakla tanımlanır METH_COEXIST
. Ayrıca __contains__()
, 2 kat daha yavaş olan diğer yöntem de aynı bayrağa sahiptir. Gönderen Resmi belgelerin :
Yöntem mevcut tanımların yerine yüklenecektir. METH_COEXIST olmadan, varsayılan tekrarlanan tanımları atlamaktır. Yuvası sarma yöntemi tablosu önce yüklenir yana, bir sq_contains yuvası varlığı, örneğin, adlı bir sarılmış yöntem yaratacak içerir () ve aynı isimde karşılık gelen bir PyCFunction yüklenmesini engellemektedir. Bayrak tanımlandığında, PyCFunction sarmalayıcı nesnesinin yerine yüklenecek ve yuva ile birlikte bulunacaktır. Bu yardımcı olur çünkü PyCFunctions çağrıları sarmalayıcı nesne çağrılarından daha iyi optimize edilmiştir.
Bu yüzden doğru anladıysam, teoride METH_COEXIST
işleri hızlandırmalı, ancak tersi bir etkiye sahip gibi görünüyor. Neden?
EDIT 2 : Daha fazlasını keşfettim.
__getitem__()
ve PyDict_Type içinde iki kez bildirildikleri için __contains()__
olarak işaretlenirler .METH_COEXIST
Her ikisi de, bir kez, tp_methods
açıkça __getitem__()
ve olarak bildirildikleri yuvada bulunurlar __contains()__
. Ama resmi belgeler söylüyor tp_methods
edilir değil alt sınıflar tarafından miras.
Yani bir alt sınıfı dict
çağırmaz __getitem__()
, ancak alt alanı çağırır mp_subscript
. Gerçekten de, bir alt sınıfın alt mp_subscript
yuvalarını tp_as_mapping
devralmasına izin veren yuvada bulunur.
Sorun hem olmasıdır __getitem__()
ve mp_subscript
kullanmak aynı işlevi dict_subscript
. Onu yavaşlatan sadece kalıtsal olarak kalması mümkün mü?
len()
örneğin, neden 2 kat daha yavaş değil, aynı hıza sahip?
len
Yerleşik dizi türleri için hızlı bir yol olması gerektiğini düşünürdüm . Sorunuza uygun bir cevap verebileceğimi sanmıyorum, ama iyi bir soru, umarım Python içselleri hakkında benden daha bilgili biri.
__contains__
uygulama, devralma için kullanılan mantığı engelliyor sq_contains
.
dict
olup olmadığını kontrol eden hızlı bir yol olduğuna inanıyorum ve eğer öyleyse,__getitem__
yöntemi aramak yerine doğrudan C uygulamasını çağırıyor nesnenin sınıfı. Bu nedenle kodunuz'__getitem__'
, sınıfA
üyeleri sözlüğündeki anahtar için birincisi olmak üzere iki dikte araması yapar , bu nedenle yaklaşık iki kat daha yavaş olması beklenebilir.pickle
Açıklama muhtemelen oldukça benzer.