पायथन में सबक्लासिंग चीजों को इतना धीमा क्यों करता है?


13

मैं एक साधारण वर्ग कि फैली हुई है पर काम कर रहा था dict, और मुझे लगता है कि कुंजी देखने और उपयोग एहसास हुआ की pickleहैं बहुत धीमी गति से।

मुझे लगा कि यह मेरी कक्षा के लिए एक समस्या है, इसलिए मैंने कुछ तुच्छ मानदंड किए:

(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

परिणाम वास्तव में एक आश्चर्य की बात है। जबकि कुंजी देखने धीमी 2x है, pickleहै 5x धीमी।

यह कैसे हो सकता है? अन्य विधियाँ, जैसे get(), __eq__()और __init__(), और पुनरावृति keys(), values()और items()जैसे ही तेज़ हैं dict


EDIT : मैंने पायथन 3.9 के स्रोत कोड पर एक नज़र डाली, और Objects/dictobject.cऐसा लगता है कि __getitem__()विधि द्वारा लागू किया गया है dict_subscript()। और dict_subscript()उपवर्गों को धीमा कर देता है यदि कुंजी गायब है, तो उपवर्ग लागू कर सकता है __missing__()और यह देखने की कोशिश करता है कि क्या यह मौजूद है। लेकिन बेंचमार्क एक मौजूदा कुंजी के साथ था।

लेकिन मैंने कुछ देखा: __getitem__()झंडे के साथ परिभाषित किया गया है METH_COEXIST। और यह भी __contains__(), दूसरी विधि जो 2x धीमी है, में एक ही झंडा है। से आधिकारिक दस्तावेज :

मौजूदा परिभाषाओं के स्थान पर विधि लोड की जाएगी। METH_COEXIST के बिना, डिफ़ॉल्ट दोहराई गई परिभाषाओं को छोड़ना है। चूँकि स्लॉट रैपर को विधि तालिका से पहले लोड किया जाता है, उदाहरण के लिए, sq_contains स्लॉट का अस्तित्व, इसमें लिपटे हुए विधि को उत्पन्न करेगा () और इसी नाम के साथ संबंधित PyCFunction के लोडिंग को रोक देगा। परिभाषित ध्वज के साथ, PyCFunction को रैपर ऑब्जेक्ट के स्थान पर लोड किया जाएगा और स्लॉट के साथ सह-अस्तित्व में होगा। यह मददगार है क्योंकि PyCFunctions के कॉल रैपर ऑब्जेक्ट कॉल से अधिक अनुकूलित हैं।

इसलिए अगर मुझे सही तरीके से समझ में आया है, तो सिद्धांत रूप में METH_COEXISTचीजों को गति देना चाहिए, लेकिन इसका विपरीत प्रभाव पड़ता है। क्यों?


EDIT 2 : मैंने कुछ और खोज की।

__getitem__()और के __contains()__रूप में चिह्नित किए METH_COEXISTजाते हैं, क्योंकि वे PyDict_Type में दो बार घोषित किए जाते हैं ।

वे दोनों मौजूद हैं, एक बार, स्लॉट में tp_methods, जहां उन्हें स्पष्ट रूप से घोषित किया गया है __getitem__()और __contains()__। लेकिन आधिकारिक दस्तावेज का कहना है कि उपवर्गों को विरासत tp_methodsमें नहीं मिला है।

तो एक उपवर्ग dictकॉल नहीं करता है __getitem__(), लेकिन उप- कॉल को कॉल करता है mp_subscript। वास्तव mp_subscriptमें, स्लॉट में निहित है tp_as_mapping, जो एक उपवर्ग को अपने सबलेट्स को प्राप्त करने की अनुमति देता है।

समस्या यह है कि दोनों है __getitem__()और mp_subscriptउपयोग करने के लिए एक ही , समारोह dict_subscript। क्या यह संभव है कि यह केवल वह तरीका है जो विरासत में मिला था जो इसे धीमा कर देता है?


5
मैं स्रोत कोड के विशिष्ट भाग को खोजने में सक्षम नहीं हूं, लेकिन मेरा मानना ​​है कि सी कार्यान्वयन में एक तेज़ पथ है जो जाँचता है कि क्या वस्तु एक है dictऔर यदि ऐसा है, तो __getitem__विधि को देखने के बजाय सी कार्यान्वयन को सीधे कॉल करता है । वस्तु का वर्ग। इसलिए आपका कोड दो प्रतिष्ठित लुकअप करता है, '__getitem__'वर्ग Aके सदस्यों के शब्दकोश में कुंजी के लिए पहला एक है , इसलिए यह लगभग दो बार धीमा होने की उम्मीद की जा सकती है। pickleविवरण शायद काफी समान है।
काया ३५

@ kaya3: लेकिन अगर ऐसा है, तो, len()उदाहरण के लिए, 2x धीमा क्यों नहीं है, लेकिन समान गति है?
मार्को सुल

मैं इस बारे में निश्चित नहीं हूं; मैंने सोचा होगा कि lenअंतर्निहित अनुक्रम प्रकारों के लिए एक तेज़ पथ होना चाहिए। मुझे नहीं लगता कि मैं आपके प्रश्न का उचित उत्तर देने में सक्षम हूं, लेकिन यह एक अच्छा है, इसलिए उम्मीद है कि कोई और मेरे बारे में पायथन इंटर्न के बारे में अधिक जानकार होगा।
काया ३५

मैंने कुछ जांच की है और प्रश्न को अद्यतन किया है।
मार्को सुल

1
... ओह। मुझे अब दिख रहा है। स्पष्ट __contains__कार्यान्वयन विरासत के लिए उपयोग किए जाने वाले तर्क को रोक रहा है sq_contains
user2357112

जवाबों:


7

अनुक्रमण और inसबक्लासेस में धीमे होते हैं dictक्योंकि एक dictऑप्टिमाइज़ेशन और लॉजिक सबक्लासेस के बीच एक खराब बातचीत के कारण C स्लॉट्स का उपयोग किया जाता है। यह निश्चित होना चाहिए, हालांकि आपके अंत से नहीं।

CPython कार्यान्वयन में ऑपरेटर ओवरलोड के लिए हुक के दो सेट हैं। पाइथन-स्तर के तरीके हैं जैसे __contains__और __getitem__, लेकिन एक प्रकार की वस्तु के मेमोरी लेआउट में सी फ़ंक्शन पॉइंटर्स के लिए स्लॉट्स का एक अलग सेट भी है। आमतौर पर, या तो पायथन विधि सी कार्यान्वयन के चारों ओर एक आवरण होगी, या सी स्लॉट में एक फ़ंक्शन होगा जो पायथन विधि को खोजता है और कॉल करता है। यह सी स्लॉट के लिए सीधे ऑपरेशन को लागू करने के लिए अधिक कुशल है, क्योंकि सी स्लॉट वह है जो वास्तव में पायथन तक पहुंचता है।

सी में लिखे मैपिंग सी स्लॉट को लागू sq_containsऔर mp_subscriptप्रदान करने के लिए inऔर अनुक्रमण। आमतौर पर, अजगर स्तर के __contains__और __getitem__तरीकों स्वचालित रूप से चारों ओर सी कार्यों रैपर के रूप में उत्पन्न किया जा सकता है, लेकिन dictवर्ग है स्पष्ट कार्यान्वयन की __contains__और __getitem__, क्योंकि स्पष्ट कार्यान्वयन एक सा उत्पन्न रैपर की तुलना में तेजी कर रहे हैं:

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

(वास्तव में, स्पष्ट __getitem__कार्यान्वयन कार्यान्वयन के समान कार्य है mp_subscript, बस एक अलग प्रकार के आवरण के साथ।)

आमतौर पर, एक उपवर्ग की तरह सी स्तर हुक के अपने माता-पिता के कार्यान्वयन के वारिस हैं sq_containsऔर mp_subscript, और उपवर्ग बस के रूप में तेजी से सुपर क्लास के रूप में किया जाएगा। हालाँकि, update_one_slotMRO खोज के माध्यम से जनरेट किए गए रैपर विधियों को खोजने का प्रयास करके पैरेंट कार्यान्वयन के लिए लग रहा है।

dictके लिए रैपर उत्पन्न नहीं किया हैsq_contains और mp_subscript, क्योंकि यह स्पष्ट __contains__और __getitem__कार्यान्वयन प्रदान करता है ।

विरासत के बजाय sq_containsऔर mp_subscript, update_one_slotउन उपवर्गों sq_containsऔर mp_subscriptकार्यान्वयनों को समाप्त करता है जो MRO खोज करते हैं __contains__और __getitem__जिन्हें कॉल करते हैं। यह सीधे सी स्लॉट्स को विरासत में देने की तुलना में बहुत कम कुशल है।

इसे ठीक करने से update_one_slotकार्यान्वयन में परिवर्तन की आवश्यकता होगी ।


जैसा कि मैंने ऊपर वर्णित किया है, इसके अलावा, तानाशाही उपवर्गों के लिए dict_subscriptभी दिखता है __missing__, इसलिए स्लॉट वंशानुक्रम समस्या को ठीक करने से dictलुकअप की गति के लिए उपवर्ग पूरी तरह से अपने आप पर नहीं बनेंगे , लेकिन यह उन्हें बहुत करीब होना चाहिए।


पिकिंग के लिए, dumpsओर से, अचार के कार्यान्वयन में डक्ट्स के लिए एक समर्पित तेज़ पथ है, जबकि तानाशाह उपवर्ग के माध्यम से object.__reduce_ex__और अधिक गोल चक्कर रास्ता लेता है save_reduce

पर loadsपक्ष, समय अंतर सिर्फ अतिरिक्त opcodes और लुकअप निकालते हैं और दृष्टांत से ज्यादातर है __main__.A, वर्ग, जबकि dicts एक नया dict बनाने के लिए एक समर्पित अचार opcode है। अगर हम अचार के लिए डिस्सैडफॉर्म की तुलना करते हैं:

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

हम देखते हैं कि दोनों के बीच का अंतर यह है कि दूसरे अचार को देखने __main__.Aऔर उसे तुरंत लगाने के लिए ऑपकोड की एक पूरी गुच्छा की आवश्यकता होती है , जबकि पहला अचार सिर्फ EMPTY_DICTएक खाली ताना पाने के लिए करता है। उसके बाद, दोनों अचार एक ही कुंजी और मूल्यों को अचार ऑपरेंड स्टैक पर चलाते हैं और चलाते हैं SETITEMS


आपका बहुत बहुत धन्यवाद! क्या आपके पास कोई विचार है कि सीपीथॉन इस अजीब विरासत पद्धति का उपयोग क्यों करता है? मेरा मतलब है कि क्या घोषणा करने का कोई तरीका नहीं है __contains__()और __getitem()ऐसे में क्या उपवर्गों को विरासत में मिल सकता है? के आधिकारिक दस्तावेज में tp_methods, यह लिखा गया है methods are inherited through a different mechanism, इसलिए यह संभव लगता है।
मार्को सुल

@ मार्कोसुल्ला: __contains__और विरासत __getitem__ में मिला है, लेकिन समस्या यह है कि sq_containsऔर mp_subscriptनहीं हैं।
user2357112

एमएच, वेल .... एक पल रुकिए। मुझे लगा कि यह इसके विपरीत है। __contains__और __getitem__स्लॉट में हैं tp_methods, कि आधिकारिक डॉक्स को उपवर्गों द्वारा विरासत में नहीं मिला है। और जैसा कि आपने कहा, update_one_slotउपयोग नहीं करता है sq_containsऔर mp_subscript
मार्को सुल

खराब शब्दों में, containsऔर बाकी को बस किसी अन्य स्लॉट में स्थानांतरित नहीं किया जा सकता है, जो उपवर्गों द्वारा विरासत में मिला है?
मार्को सुल

@MarcoSulla: tp_methodsइनहेरिट नहीं की गई है, लेकिन इससे उत्पन्न पायथन विधि ऑब्जेक्ट को इस अर्थ में विरासत में मिली है कि विशेषता की पहुंच के लिए मानक एमआरओ खोज उन्हें मिल जाएगी।
user2357112
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.