Python'da alt sınıflandırma neden işleri bu kadar yavaşlatıyor?


13

Ben uzanan basit bir sınıfın üzerinde çalışıyordu dictve ben o anahtar arama ve kullanımını fark picklevardı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.cgibi 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_COEXISTiş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_methodsaçıkça __getitem__()ve olarak bildirildikleri yuvada bulunurlar __contains()__. Ama resmi belgeler söylüyor tp_methodsedilir 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_subscriptyuvalarını tp_as_mappingdevralmasına izin veren yuvada bulunur.

Sorun hem olmasıdır __getitem__()ve mp_subscriptkullanmak aynı işlevi dict_subscript. Onu yavaşlatan sadece kalıtsal olarak kalması mümkün mü?


5
Kaynak kodun belirli bir bölümünü bulamıyorum, ancak C uygulamasında nesnenin bir dictolup 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ıf Aüyeleri sözlüğündeki anahtar için birincisi olmak üzere iki dikte araması yapar , bu nedenle yaklaşık iki kat daha yavaş olması beklenebilir. pickleAçıklama muhtemelen oldukça benzer.
kaya3

@ kaya3: Ama eğer öyleyse, len()örneğin, neden 2 kat daha yavaş değil, aynı hıza sahip?
Marco Sulla

Bunun hakkında emin değilim; lenYerleş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.
kaya3

Biraz araştırma yaptım ve soruyu güncelledim.
Marco Sulla

1
... ah. Şimdi görüyorum. Açık __contains__uygulama, devralma için kullanılan mantığı engelliyor sq_contains.
user2357112 Monica

Yanıtlar:


7

Dizinleme ve optimizasyon ile C yuvalarını devralmak için kullanılan mantık alt sınıfları arasındaki kötü etkileşim nedeniyle alt sınıflarda indaha yavaştır . Bu sonlandırılmasa da düzeltilebilir olmalıdır.dictdict

CPython uygulamasında operatör aşırı yükleri için iki takım kanca bulunur. Python düzeyinde yöntemler gibi __contains__ve vardır __getitem__, ancak aynı zamanda bir yazım nesnesinin bellek düzeninde C işlev işaretçileri için ayrı bir yuva kümesi vardır. Genellikle, Python yöntemi C uygulaması çevresinde bir sarıcı olur veya C yuvası Python yöntemini arayan ve çağıran bir işlev içerir. C yuvasının işlemi doğrudan gerçekleştirmesi daha verimlidir, çünkü C yuvası Python'un gerçekte eriştiği şeydir.

C ile yazılmış eşlemeler C yuvalarını uygular sq_containsve mp_subscriptsağlar inve dizine ekler. Normalde, Python düzeyi __contains__ve __getitem__yöntemleri otomatik olarak C işlevleri etrafında sarmalayıcılar olarak oluşturulur, ancak dictsınıfın açık uygulamaları vardır __contains__ve __getitem__çünkü açık uygulamalar oluşturulan sarmalayıcılardan biraz daha hızlıdır:

static PyMethodDef mapp_methods[] = {
    DICT___CONTAINS___METHODDEF
    {"__getitem__", (PyCFunction)(void(*)(void))dict_subscript,        METH_O | METH_COEXIST,
     getitem__doc__},
    ...

(Aslında, açık __getitem__uygulama mp_subscriptyalnızca farklı türde bir sarıcı ile uygulama ile aynı işlevdir .)

Normalde, bir alt sınıf, ebeveyninin sq_containsve gibi C düzeyi kanca uygulamalarını devralır mp_subscriptve alt sınıf da üst sınıf kadar hızlı olur. Bununla birlikte, içindeki mantık, update_one_slotbir MRO araması yoluyla üretilen sarma yöntemlerini bulmaya çalışarak üst uygulamayı arar.

dictvermez var için oluşturulan sarmalayıcılarını sq_containsve mp_subscriptbunun açık sağladığı için, __contains__ve __getitem__uygulamaları.

Devralmak yerine sq_containsve mp_subscript, update_one_slotalt sınıf vazgeçmeden uçları sq_containsve mp_subscriptbir MRO aramak yerine uygulamaları __contains__ve __getitem__ve bu ara. Bu, C yuvalarını doğrudan miras almaktan çok daha az verimlidir.

Bunu düzeltmek için update_one_slotuygulamada değişiklikler yapılması gerekecektir .


Yukarıda tarif ettiğimden başka, dict_subscriptaynı zamanda __missing__dict alt sınıflarını da arar , bu nedenle slot kalıtım sorununu düzeltmek, alt sınıfları dictarama hızı için tamamen eşit hale getirmez , ancak onları daha da yakınlaştırmalıdır.


Turşuya gelince, dumpsyanda, turşu uygulamasının dikmeler için özel bir hızlı yolu vardır , dict alt sınıfı object.__reduce_ex__ve arasında daha dolambaçlı bir yol alır save_reduce.

On loadstarafında, zaman farkı sadece almak ve örneğini ekstra opcodes ve sorgularındaki çoğunlukla __main__.Adicts yeni dicti yapmak için özel bir turşu işlem kodu varken, sınıf. Turşuların sökülmesini karşılaştırırsak:

In [26]: pickletools.dis(pickle.dumps({0: 0, 1: 1, 2: 2, 3: 3, 4: 4}))                                                                                                                                                           
    0: \x80 PROTO      4
    2: \x95 FRAME      25
   11: }    EMPTY_DICT
   12: \x94 MEMOIZE    (as 0)
   13: (    MARK
   14: K        BININT1    0
   16: K        BININT1    0
   18: K        BININT1    1
   20: K        BININT1    1
   22: K        BININT1    2
   24: K        BININT1    2
   26: K        BININT1    3
   28: K        BININT1    3
   30: K        BININT1    4
   32: K        BININT1    4
   34: u        SETITEMS   (MARK at 13)
   35: .    STOP
highest protocol among opcodes = 4

In [27]: pickletools.dis(pickle.dumps(A({0: 0, 1: 1, 2: 2, 3: 3, 4: 4})))                                                                                                                                                        
    0: \x80 PROTO      4
    2: \x95 FRAME      43
   11: \x8c SHORT_BINUNICODE '__main__'
   21: \x94 MEMOIZE    (as 0)
   22: \x8c SHORT_BINUNICODE 'A'
   25: \x94 MEMOIZE    (as 1)
   26: \x93 STACK_GLOBAL
   27: \x94 MEMOIZE    (as 2)
   28: )    EMPTY_TUPLE
   29: \x81 NEWOBJ
   30: \x94 MEMOIZE    (as 3)
   31: (    MARK
   32: K        BININT1    0
   34: K        BININT1    0
   36: K        BININT1    1
   38: K        BININT1    1
   40: K        BININT1    2
   42: K        BININT1    2
   44: K        BININT1    3
   46: K        BININT1    3
   48: K        BININT1    4
   50: K        BININT1    4
   52: u        SETITEMS   (MARK at 31)
   53: .    STOP
highest protocol among opcodes = 4

ikisinin arasındaki fark, ikinci turşunun bakmak __main__.Ave örneklemek için bir sürü opcode ihtiyaç duyduğunu , ilk turşunun ise EMPTY_DICTboş bir dikte almak olduğunu görüyoruz. Bundan sonra, her iki turşu da aynı anahtarları ve değerleri turşu işlenen yığınına iter ve çalıştırır SETITEMS.


Çok teşekkür ederim! CPython'un neden bu garip miras yöntemini kullandığına dair bir fikrin var mı? Ben bildirmek için bir yol yoktur, demek __contains__()ve __getitem()alt sınıflar tarafından miras edilebilir şekilde? Resmi belgelerinde tp_methodsyazılmıştır methods are inherited through a different mechanism, bu yüzden mümkün görünüyor.
Marco Sulla

@MarcoSulla: __contains__ve __getitem__ edilir miras, ancak sorun olduğunu sq_containsve mp_subscriptdeğillerdir.
user2357112 Monica

Mh, peki .... bir dakika. Aksine düşündüm. __contains__ve __getitem__yuvaya olan tp_methodsresmi dokümanlar için alt sınıflar tarafından miras olmadığını,. Ve dediğin gibi update_one_slot, sq_containsve kullanmaz mp_subscript.
Marco Sulla

Zayıf kelimelerle, containsgeri kalanı alt sınıflar tarafından miras alınan başka bir yuvaya taşınamaz mı?
Marco Sulla

@MarcoSulla: tp_methodsdevralınmaz, ancak ondan oluşturulan Python yöntemi nesneleri, öznitelik erişimi için standart MRO aramasının onları bulacağı anlamıyla miras alınır.
user2357112 Monica
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.