= len("#") && s[:l...">

गो में कार्यों की दोषपूर्ण एम्बेडिंग


क्या कोड प्रदर्शन में नीचे दिखाया गया है?


// (A).  HasPrefix  . return strings.HasPrefix(s, "#") // (B).    HasPrefix. return len(s) >= len("#") && s[:len("#")] == "#" 

जवाब है नहीं


विवरण और स्पष्टीकरण के लिए, मैं बिल्ली के नीचे पूछता हूं।




अच्छा दिन, इससे पहले कि आप विषय खोलें, मैं अपना परिचय देना चाहूंगा।
मेरा नाम इस्कंदर है और समय-समय पर मैं गोलांग / गो रिपॉजिटरी को कमिट भेजता हूं।


छवि

मैं इंटेल गो टीम की ओर से ऐसा करता था, लेकिन हमारे रास्तों में बदलाव आया और अब मैं एक स्वतंत्र योगदानकर्ता हूं। हाल ही में मैं बुनियादी ढांचे की टीम में वीके में काम कर रहा हूं।


अपने खाली समय में मैं गो के लिए अलग उपकरण बनाता हूं, जैसे कि गो-आलोचक और गो-संगत । मैं गोफर्स भी आकर्षित करता हूं।




इसे मापें!


तुलना करने के लिए तुरंत आगे बढ़ें और बेंचमार्क को रेखांकित करें:


 package benchmark import ( "strings" "testing" ) var s = "#string" func BenchmarkHasPrefixCall(b *testing.B) { for i := 0; i < bN; i++ { _ = strings.HasPrefix(s, "#") _ = strings.HasPrefix(s, "x") } } func BenchmarkHasPrefixInlined(b *testing.B) { for i := 0; i < bN; i++ { _ = len(s) >= len("#") && s[:len("#")] == "#" _ = len(s) >= len("x") && s[:len("x")] == "x" } } 

मैं आपको पीठ थपथपाने की सिफारिश करने के बजाय, आपको बेंचमार्क दिखाऊंगा


एक आदेश के साथ, हम दोनों बेंचमार्क चला सकते हैं और एक तुलना प्राप्त कर सकते हैं:


 go-benchrun HasPrefixCall HasPrefixInlined -v -count=10 . Benchstat results: name old time/op new time/op delta HasPrefixCall-8 9.15ns ± 1% 0.36ns ± 3% -96.09% (p=0.000 n=10+9) 

संकलक के साथ फ़ंक्शन बॉडी को एम्बेड करके प्राप्त कोड की तुलना में मैन्युअल एम्बेडिंग के साथ विकल्प बहुत तेज़ है। आइए जानने की कोशिश करते हैं कि ऐसा क्यों हो रहा है।


strings.HasPrefix


स्ट्रिंग्स के कार्यान्वयन को याद रखें।


 // HasPrefix tests whether the string s begins with prefix. func HasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[0:len(prefix)] == prefix } 

HasPrefix फ़ंक्शन कंपाइलर द्वारा बनाया गया है।
आप इसे इस प्रकार सत्यापित कर सकते हैं:


 go build -gcflags='-m=2' strings 2>&1 | grep 'can inline HasPrefix' 

strings.HasPrefix को कॉल करने के लिए। विकल्प से strings.HasPrefix (A) हम निम्नलिखित मशीन कोड प्राप्त करते हैं:


  MOVQ (TLS), CX CMPQ SP, 16(CX) JLS more_stack fn_body: SUBQ $40, SP MOVQ BP, 32(SP) LEAQ 32(SP), BP XCHGL AX, AX MOVQ s+56(SP), AX CMPQ AX, $1 JGE compare_strings XORL AX, AX MOVB AL, ~ret1+64(SP) MOVQ 32(SP), BP ADDQ $40, SP return: RET compare_strings: MOVQ s+48(SP), AX MOVQ AX, (SP) LEAQ go.string."#"(SB), AX MOVQ AX, 8(SP) MOVQ $1, 16(SP) CALL runtime.memequal(SB) MOVBLZX 24(SP), AX JMP return more_stack: CALL runtime.morestack_noctxt(SB) JMP fn_body 

इस तथ्य को अनदेखा करें कि कोड नूडल्स की तरह दिखता है।


आपको किन बातों पर ध्यान देना चाहिए:


  • strings.HasPrefix वास्तव में डाला गया था, कोई कॉल नहीं।
  • स्ट्रिंग्स की तुलना करने के लिए, runtime.memequalruntime.memequal

लेकिन तब मैन्युअल रूप से निर्मित संस्करण, उदाहरण के लिए कोड (B) लिए क्या उत्पन्न होता है?


  MOVQ s+16(SP), AX CMPQ AX, $1 JLT different_length MOVQ s+8(SP), AX CMPB (AX), $35 // 35 -   "#" SETEQ AL return: MOVB AL, "".~ret1+24(SP) RET different_length: XORL AX, AX JMP 22 

और यहाँ संकलक runtime.memequal लिए कॉल उत्पन्न नहीं करता है और किसी एकल वर्ण की सीधे तुलना की जाती है। आदर्श रूप से, उन्हें पहले विकल्प के लिए ऐसा ही करना चाहिए था।


हम गो ऑप्टिमाइज़र के कमजोर पक्ष का निरीक्षण करते हैं, और हम इसका विश्लेषण करेंगे।


लगातार अभिव्यक्ति अनुकूलन


वह कारण जो strings.HasPrefix(s, "#") बुलाता है। strings.HasPrefix(s, "#") को अनुकूलित किया जा सकता है क्योंकि उपसर्ग तर्क एक स्थिर है। हम इसकी लंबाई और सामग्री जानते हैं। शॉर्ट स्ट्रिंग्स के लिए runtime.memequal को कॉल करने का कोई मतलब नहीं है, यह अतिरिक्त कॉल से बचने के लिए "जगह में" पात्रों की तुलना करने के लिए तेज़ है।


जैसा कि आप जानते हैं, आमतौर पर कंपाइलरों में कम से कम दो भाग होते हैं: कंपाइलर फ्रंटेंड और कंपाइलर बैकेंड । पहला उच्च स्तर के दृश्य के साथ काम करता है, दूसरा मशीन के करीब है और मध्यवर्ती दृश्य निर्देशों की एक धारा की तरह दिखेगा। गो के कई संस्करणों ने पहले ही संकलक के बैकएंड भाग में अनुकूलन के लिए एसएसए प्रतिनिधित्व का उपयोग किया है।


लगातार तह, जैसे कि {10*2 => 20} , बैकएंड में लागू किया जाता है। सामान्य तौर पर, अभिव्यक्तियों की कम्प्यूटेशनल लागत को कम करने से जुड़े अधिकांश ऑपरेशन कंपाइलर के इस हिस्से में स्थित होते हैं। लेकिन अपवाद हैं।


एक अपवाद निरंतर स्ट्रिंग तुलनाओं का अनुकूलन है। जब कंपाइलर एक स्ट्रिंग (या सबस्ट्रिंग) की तुलना करता है, जिसमें एक या दोनों ऑपरेंड स्थिर होते हैं, तो कॉलटाइम से अधिक कुशल कोड उत्पन्न होता है।


आप इसके लिए स्रोत कोड को cmd / compile / आंतरिक / gc / walk.go: 3362 में देख सकते हैं


इन अनुकूलन शुरू होने से पहले फ़ंक्शन एम्बेडिंग होती है, लेकिन संकलक के सामने वाले हिस्से में भी।


ऐसा लगता है कि सभी समान इस अनुकूलन को हमारे मामले में काम करने की अनुमति नहीं देते हैं?


एंबेड फंक्शन कॉल्स कैसे जाएं


यहां बताया गया है कि एम्बेडिंग कैसे होगी:


 //    : return strings.HasPrefix(s, "#") //  : func HasPrefix(s, prefix string) bool //    : _s, _prefix := s, "#" return len(s) >= len(prefix) && s[:len(prefix)] == prefix 

जब एम्बेडिंग फ़ंक्शंस, संकलक अस्थायी चर के लिए तर्क देता है, जो अनुकूलन को तोड़ता है, चूंकि walk.go में एल्गोरिथ्म स्थिरांक नहीं देखता है, लेकिन चर के साथ तर्क। यही समस्या है।


वैसे, यह बैकएंड अनुकूलन के साथ हस्तक्षेप नहीं करता है जो एसएसए के पास अपने निपटान में है। लेकिन वहाँ अन्य समस्याएं हैं, उदाहरण के लिए, उनकी प्रभावी तुलना के लिए उच्च-स्तरीय भाषा निर्माणों को पुनर्स्थापित करने में असमर्थता (कई वर्षों से इस खामी को खत्म करने का काम "नियोजित" किया गया है)।


एक और उदाहरण: विश्लेषण से बच


एक फ़ंक्शन की कल्पना करें जो स्टैक पर एक अस्थायी बफर आवंटित करना महत्वपूर्ण है:


 func businessLogic() error { buf := make([]byte, 0, 16) // buf    //    . return nil } 

चूंकि buf "दूर नहीं भागता है", संकलक ढेर पर आवंटन के बिना, इन 16 बाइट्स को स्टैक पर आवंटित करने में सक्षम होगा। फिर, कॉल करते समय निरंतर मूल्य के लिए सभी धन्यवाद। स्टैक पर मेमोरी आवंटित करने के लिए, हमारे लिए आवश्यक आकार जानना आवश्यक है, जो फ़ंक्शन कॉल को आवंटित फ्रेम का हिस्सा होगा।


मान लीजिए कि भविष्य में हम विभिन्न आकारों के अस्थायी बफ़र्स आवंटित करना चाहते थे और तरीकों में कुछ तर्क को अतिक्रमण करना चाहते थे। हमने एक नई अमूर्तता शुरू की और tmpBuf नए tmpBuf प्रकार का उपयोग करने का निर्णय लिया। डिजाइन समारोह अत्यंत सरल है:


 func newTmpBuf(sizeHint int) tmpBuf { return tmpBuf{buf: make([]byte, 0, sizeHint)} } 

मूल उदाहरण को अपनाना:


 func businessLogic() error { - buf := make([]byte, 0, 16) + buf := newTmpBuf(16) // buf    //    . return nil } 

कंस्ट्रक्टर को एम्बेड किया जाएगा, लेकिन आवंटन अब हमेशा ढेर पर होगा, इसी कारण से तर्क अस्थायी चर के माध्यम से पारित किए जाते हैं। एस्केप एनालिसिस make([]byte, 0, _sizeHint) जो कि अनुकूलित make कॉल के लिए इसके मान्यता पैटर्न के अंतर्गत नहीं आते हैं।


अगर हमारे पास "लोगों की तरह सब कुछ" था, तो समस्या मौजूद नहीं होगी, newTmpBuf कंस्ट्रक्टर को एम्बेड करने के बाद newTmpBuf यह स्पष्ट होगा कि आकार अभी भी संकलन चरण में जाना जाता है।


यह तार की तुलना के साथ स्थिति से लगभग अधिक है।


क्षितिज १.१३


स्थिति को आसानी से ठीक किया जा सकता है और मैंने पहले ही निर्णय का पहला भाग भेज दिया है


छवि

अगर आपको लगता है कि लेख में वर्णित समस्या को वास्तव में समाधान की आवश्यकता है, तो कृपया इसी मुद्दे में एक अंगूठे को रखें।





मेरी स्थिति यह है कि आपके हाथों से कोड एम्बेड करना सिर्फ इसलिए कि यह गो के वर्तमान संस्करण में तेजी से काम करता है, गलत है। ऑप्टिमाइज़र में इस दोष को ठीक करना आवश्यक है, कम से कम उस बिंदु पर जहां अप्रत्याशित प्रदर्शन प्रतिगमन के बिना काम के ऊपर वर्णित उदाहरण हैं।


यदि सब कुछ योजना के अनुसार होता है, तो यह अनुकूलन गो 1.13 रिलीज़ में शामिल किया जाएगा।


आपका ध्यान के लिए धन्यवाद।


जोड़: प्रस्तावित समाधान


यह खंड सबसे बहादुर लोगों के लिए है, जो पढ़ने से नहीं थकते हैं।


तो, हमारे पास कई जगह हैं जो सीधे अपने मूल्यों के बजाय चर का उपयोग करते समय बदतर काम करते हैं। प्रस्तावित समाधान संकलक भाग के सामने एक नया कार्य शुरू करना है, जो आपको नाम से अंतिम बाध्य मान प्राप्त करने की अनुमति देता है। उसके बाद, प्रत्येक अनुकूलन में जो एक निरंतर मूल्य की अपेक्षा करता है, एक चर का पता चलने पर हार नहीं मानता है, लेकिन यह पहले से सहेजे गए राज्य को प्राप्त करता है।


हमारी नई सुविधा के हस्ताक्षर इस तरह दिख सकते हैं:


 func getConstValue(n *Node) *Node 

Node की परिभाषा वाक्यविन्यास फ़ाइल में पाई जा सकती है।


प्रत्येक चर परिभाषा में ONAME टैग के साथ एक Node टैग होता है। Node.Name.Defn अंदर Node.Name.Defn इनमें से अधिकांश चरों का आरंभिक मूल्य है।


यदि Node पहले Node ही एक शाब्दिक है, तो आपको कुछ भी करने की आवश्यकता नहीं है और हम सिर्फ n वापस करते हैं। यदि यह ONAME (वैरिएबल) है, तो आप n.Name.Defn से समान इनिशियलाइज़िंग मान निकालने का प्रयास कर सकते हैं।


लेकिन एक चर को घोषित करने और पढ़ने के बीच संशोधनों के बारे में क्या है जिसके लिए हम getConstValue कहते हैं? यदि हम खुद को केवल पढ़ने के लिए चर तक सीमित रखते हैं, तो कोई समस्या नहीं है। गो के फ्रंट में पहले से ही विशेष नोड फ़्लैग हैं जो समान नामों को चिह्नित करते हैं। यदि चर को संशोधित किया गया है, तो getConstValue एक आरंभीकरण मान नहीं लौटाएगा।


प्रोग्रामर, एक नियम के रूप में, संख्यात्मक और स्ट्रिंग प्रकारों के इनपुट तर्कों को संशोधित नहीं करते हैं, और यह इस आदिम एल्गोरिदम के साथ बड़ी संख्या में मामलों को कवर करना संभव बनाता है।


अब हम कार्यान्वयन पर विचार करने के लिए तैयार हैं:


 func getConstValue(n *Node) *Node { //    ONAME    definition. if n.Op != ONAME || n.Name.Defn == nil { return n } //   ,     . // ,    ,     //      escape analysis' . maybeModified := n.Assigned() || n.Name.Defn.Assigned() || n.Addrtaken() if maybeModified { return n } // OAS - Node  . // n.Name.Defn.Left -  LHS. // n.Name.Defn.Right -  RHS. // consttype(v)     . //   CTxxx,      . if n.Name.Defn.Op == OAS { v := n.Name.Defn.Right if v != nil && consttype(v) != CTxxx { return v } } return n } 

यहां बताया गया है कि कोड कैसे बदलता है, जो स्थिरांक पर निर्भर करता है:


 - i := indexconst(r) + i := indexconst(getConstValue(r)) 

महान, और यह भी काम करता है:


 n := 10 xs := make([]int, n) //     ! 

इस परिवर्तन से पहले, एस्केप विश्लेषण को n माध्यम से मान 10 नहीं मिल सकता था, यही कारण है कि मैंने ढेर पर xs लगाने की आवश्यकता के बारे में एक धारणा बनाई।


उपरोक्त कोड एम्बेडिंग के दौरान देखी गई स्थिति के समान ही वाक्यात्मक है। n एक अस्थायी चर हो सकता है जो तर्क पास होने पर जोड़ा जाता है।


दुर्भाग्य से, वहाँ बारीकियों हैं।


हमने OAS के माध्यम से पेश किए गए स्थानीय चरों के लिए समस्या को हल किया, लेकिन गो OAS2 के माध्यम से अंतर्निहित कार्यों के लिए चर को आरंभीकृत करता है। इस वजह से, हमें एक दूसरे बदलाव की आवश्यकता है जो getConstValue फ़ंक्शन को बढ़ाता है और getConstValue के कोड को थोड़ा संशोधित करता है, क्योंकि, अन्य बातों के अलावा, OAS2 में एक उपयुक्त Defn फ़ील्ड नहीं है।


वह बुरी खबर थी। अच्छी खबर: #gocontributing चैनल रूसी-भाषा के सुस्त पड़ाव में दिखाई दिया, जहाँ आप अपने विचारों और योजनाओं को साझा कर सकते हैं, सवाल पूछ सकते हैं, और गो विकास में भाग लेने से संबंधित हर चीज पर चर्चा कर सकते हैं।

Source: https://habr.com/ru/post/hi438636/


All Articles