क्यों लेक्सिकल स्कोप के साथ `लेट’ तेज है?


31

dolistमैक्रो के लिए स्रोत कोड पढ़ते समय , मैं निम्नलिखित टिप्पणी में भाग गया।

;; यह एक विश्वसनीय परीक्षण नहीं है, लेकिन इससे कोई फर्क नहीं पड़ता क्योंकि दोनों शब्दार्थ स्वीकार्य हैं, थियो एक गतिशील स्कूपिंग के साथ थोड़ा तेज है और दूसरा थोड़ा तेज है (और इसमें क्लीनर सेमेंटिक्स है) लेक्सिकल स्कूपिंग के साथ

जो इस स्निपेट को संदर्भित करता है (जो मैंने स्पष्टता के लिए सरलीकृत किया है)।

(if lexical-binding
    (let ((temp list))
      (while temp
        (let ((it (car temp)))
          ;; Body goes here
          (setq temp (cdr temp)))))
  (let ((temp list)
        it)
    (while temp
      (setq it (car temp))
      ;; Body goes here
      (setq temp (cdr temp)))))

इसने मुझे letएक लूप के अंदर इस्तेमाल होने वाले फॉर्म को देखकर आश्चर्यचकित कर दिया । मुझे लगता है कि setqएक ही बाहरी चर पर बार-बार उपयोग करने की तुलना में यह धीमा है (जैसा कि ऊपर दूसरे मामले पर किया गया है)।

मुझे लगता है कि कुछ भी नहीं के रूप में खारिज कर दिया होगा, अगर ऊपर टिप्पणी के लिए तुरंत नहीं यह स्पष्ट रूप से कह रहा है कि विकल्प की तुलना में तेज है (लेक्सिकल बाइंडिंग के साथ)। तो ... ऐसा क्यों है?

  1. लेक्सिकल बनाम डायनामिक बाइंडिंग पर प्रदर्शन में कोड ऊपर क्यों भिन्न होता है?
  2. letलेक्सिकल के साथ फॉर्म क्यों तेज है?

जवाबों:


38

सामान्य रूप से लेसिक बाइंडिंग बनाम डायनेमिक बाइंडिंग

निम्नलिखित उदाहरण पर विचार करें:

(let ((lexical-binding nil))
  (disassemble
   (byte-compile (lambda ()
                   (let ((foo 10))
                     (message foo))))))

यह संकलन करता है और तुरंत lambdaएक स्थानीय चर के साथ एक सरल को मिटा देता है। lexical-bindingऊपर के रूप में विकलांग के साथ , बाइट कोड निम्नानुसार दिखता है:

0       constant  10
1       varbind   foo
2       constant  message
3       varref    foo
4       call      1
5       unbind    1
6       return    

नोट varbindऔर varrefनिर्देश। ये निर्देश ढेर स्मृति पर एक वैश्विक बाध्यकारी वातावरण में उनके नाम से क्रमशः चर और बाँधते हैं । यह सब प्रदर्शन पर प्रतिकूल प्रभाव डालता है: इसमें स्ट्रिंग हैशिंग और तुलना , वैश्विक डेटा एक्सेस के लिए सिंक्रनाइज़ेशन , और दोहराया हीप मेमोरी एक्सेस शामिल है जो सीपीयू कैशिंग के साथ बुरी तरह से खेलता है। इसके अलावा, डायनामिक वैरिएबल बाइंडिंग को अपने पिछले वैरिएबल के अंत में पुनर्स्थापित करने की आवश्यकता होती है , जो बिंदी के साथ प्रत्येक ब्लॉक के लिए अतिरिक्त लुकअप जोड़ता है ।letnletn

यदि आप बाँध lexical-bindingके tऊपर के उदाहरण में, बाइट कोड कुछ भिन्न दिखेगा:

0       constant  10
1       constant  message
2       stack-ref 1
3       call      1
4       return    

ध्यान दें कि varbindऔर varrefपूरी तरह से चले गए हैं। स्थानीय चर को केवल स्टैक पर धकेल दिया जाता है, और stack-refनिर्देश के माध्यम से एक स्थिर ऑफसेट द्वारा संदर्भित किया जाता है । अनिवार्य रूप से, चर बाध्य है और लगातार समय के साथ पढ़ा जाता है , इन-स्टैक मेमोरी पढ़ता है और लिखता है, जो पूरी तरह से स्थानीय है और इस तरह से संगामिति और सीपीयू कैशिंग के साथ अच्छा खेलता है , और इसमें कोई तार शामिल नहीं है।

आम तौर पर, (उदाहरण के लिए स्थानीय चर का शाब्दिक बाध्यकारी लुकअप के साथ let, setq, आदि) बहुत कम क्रम और स्मृति जटिलता

यह विशिष्ट उदाहरण है

डायनामिक बाइंडिंग के साथ, प्रत्येक को उपरोक्त कारणों से एक प्रदर्शन दंड दिया जाता है। अधिक देता है, अधिक गतिशील चर बाइंडिंग।

विशेष रूप से, शरीर के letभीतर एक अतिरिक्त के साथ loop, बाउंड चर को लूप के प्रत्येक पुनरावृत्ति पर बहाल करना होगा, प्रत्येक पुनरावृत्ति के लिए एक अतिरिक्त चर लुकअप को जोड़ना होगा । इसलिए, यह लूप बॉडी से बाहर रखने के लिए तेज़ है, ताकि संपूर्ण लूप समाप्त होने के बाद पुनरावृति चर केवल एक बार रीसेट हो। हालाँकि, यह विशेष रूप से सुरुचिपूर्ण नहीं है, क्योंकि पुनरावृत्ति चर बाउंड तरीका है इससे पहले कि यह वास्तव में आवश्यक है।

लेक्सिकल बाइंडिंग के साथ, letएस सस्ते हैं। विशेष रूप से, एक letलूप बॉडी के भीतर लूप बॉडी के letबाहर की तुलना में बदतर (प्रदर्शन-वार) नहीं है । इसलिए, चरों को स्थानीय रूप से बाँधना पूरी तरह से ठीक है, और लूप बॉडी तक सीमित होने वाले वैरिएबल वैरिएबल को रखना।

यह थोड़ा तेज़ भी है, क्योंकि यह बहुत कम निर्देशों का संकलन करता है। साइड-बाय-साइड डिस्सैम्ड के बाद के भाग पर विचार करें (दाईं ओर स्थानीय लेट):

0       varref    list            0       varref    list         
1       constant  nil             1:1     dup                    
2       varbind   it              2       goto-if-nil-else-pop 2 
3       dup                       5       dup                    
4       varbind   temp            6       car                    
5       goto-if-nil-else-pop 2    7       stack-ref 1            
8:1     varref    temp            8       cdr                    
9       car                       9       discardN-preserve-tos 2
10      varset    it              11      goto      1            
11      varref    temp            14:2    return                 
12      cdr       
13      dup       
14      varset    temp
15      goto-if-not-nil 1
18      constant  nil
19:2    unbind    2
20      return    

मेरे पास कोई सुराग नहीं है, हालांकि, क्या अंतर पैदा कर रहा है।


7

संक्षेप में - गतिशील बंधन बहुत धीमा है। लेक्सिकल बाइंडिंग रनटाइम में बेहद तेज है। मौलिक कारण यह है कि संकलित बाध्यकारी समय को संकलित किया जा सकता है, जबकि गतिशील बाध्यकारी नहीं हो सकता है।

निम्नलिखित कोड पर विचार करें:

(let ((x 42))
    (foo)
    (message "%d" x))

जब इसका संकलन होता है let, तो संकलक यह नहीं जान सकता है कि क्या foo(डायनामिक रूप से बाध्य) वैरिएबल को एक्सेप्ट करेगा x, इसलिए इसे के लिए एक बाइंडिंग बनाना होगा x, और वेरिएबल के नाम को बनाए रखना होगा। लेक्सिकल बाइंडिंग के साथ, कंपाइलर सिर्फ अपने नाम के बिना, बाइंडिंग स्टैक पर मूल्य को डंप करता है x, और सीधे प्रवेश को एक्सेस करता है।

लेकिन रुकिए - और भी है। लेक्सिकल बाइंडिंग के साथ, कंपाइलर यह सत्यापित करने में सक्षम है कि यह विशेष बाइंडिंग xकेवल कोड में उपयोग किया जाता है message; चूंकि xयह कभी संशोधित नहीं होता है, यह इनलाइन xऔर उपज के लिए सुरक्षित है

(progn
  (foo)
  (message "%d" 42))

मुझे नहीं लगता कि वर्तमान बाइटकोड कंपाइलर इस अनुकूलन को करता है, लेकिन मुझे विश्वास है कि यह भविष्य में ऐसा करेगा।

तो संक्षेप में:

  • डायनामिक बाइंडिंग एक भारी वजन वाला ऑपरेशन है जो अनुकूलन के लिए कुछ ऑपॉर्ट्यूनिटीज की अनुमति देता है;
  • लेक्सिकल बाइंडिंग एक हल्का ऑपरेशन है;
  • केवल-पढ़ने के मूल्य का शाब्दिक बंधन अक्सर दूर अनुकूलित किया जा सकता है।

3

यह टिप्पणी यह ​​नहीं बताती है कि गतिशील बंधन की तुलना में लेक्सिकल बाइंडिंग या तो तेज या धीमी है। इसके बजाय, यह बताता है कि उन विभिन्न रूपों में शाब्दिक और गतिशील बंधन के तहत अलग-अलग प्रदर्शन विशेषताएं हैं, उदाहरण के लिए, उनमें से एक बाध्यकारी अनुशासन के तहत बेहतर है, और दूसरे में बेहतर है।

तो क्या डायनेमिक स्कोप डायनेमिक स्कोप से तेज है? मुझे संदेह है कि इस मामले में कि बहुत अंतर नहीं है, लेकिन मुझे नहीं पता - आपको वास्तव में इसे मापना होगा।


1
varbindलेक्सिकल बाइंडिंग के तहत संकलित कोड में नहीं है। यही संपूर्ण बिंदु और उद्देश्य है।
चंद्र

हम्म। मैंने उपरोक्त स्रोत से युक्त एक फ़ाइल बनाई, जिसके साथ शुरुआत की ;; -*- lexical-binding: t -*-, इसे लोड किया, और कॉल किया (byte-compile 'sum1), यह मानते हुए कि लेक्सिकल बाइंडिंग के तहत संकलित एक परिभाषा का उत्पादन किया। हालाँकि, ऐसा लगता नहीं है।
gsg

बाइट कोड टिप्पणियों को हटा दिया क्योंकि वे उस गलत धारणा पर आधारित थे।
gsg

lunaryon के जवाब से पता चलता है कि इस कोड स्पष्ट रूप से है बाध्यकारी शाब्दिक के तहत तेजी से (हालांकि निश्चित रूप से केवल सूक्ष्म स्तर पर)।
शास्त्री

@gsg यह घोषणा सिर्फ एक मानक फ़ाइल चर है, जिसका संगत फ़ाइल बफर के बाहर से लागू किए गए कार्यों पर कोई प्रभाव नहीं पड़ता है। IOW, इसका केवल तभी प्रभाव होता है जब आप स्रोत फ़ाइल पर जाते हैं और फिर byte-compileसंबंधित बफर के करंट के साथ आह्वान करते हैं, जो है- वैसे-वैसे जो बाइट कंपाइलर कर रहा है। यदि आप byte-compileअलग से आह्वान करते हैं, तो आपको स्पष्ट रूप से निर्धारित करने की आवश्यकता है lexical-binding, जैसा कि मैंने अपने उत्तर में किया था।
चंद्रग्रहण
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.