OpenResty में `string` के फायदे और नुकसान
API7.ai
December 8, 2022
पिछले लेख में, हमने OpenResty में सामान्य ब्लॉकिंग फ़ंक्शन से परिचित हुए, जिनका उपयोग अक्सर शुरुआती लोग गलत तरीके से करते हैं। इस लेख से शुरू करके, हम परफॉर्मेंस ऑप्टिमाइज़ेशन के मूल में जाएंगे, जिसमें बहुत सारे ऑप्टिमाइज़ेशन तकनीक शामिल होंगे जो हमें OpenResty कोड की परफॉर्मेंस को तेजी से सुधारने में मदद करेंगे, इसलिए इसे हल्के में न लें।
इस प्रक्रिया में, हमें अधिक टेस्ट कोड लिखने की आवश्यकता होगी ताकि हम इन ऑप्टिमाइज़ेशन तकनीक का उपयोग करने और उनकी प्रभावशीलता को सत्यापित करने का अनुभव कर सकें, ताकि हम उनका अच्छी तरह से उपयोग कर सकें।
परफॉर्मेंस ऑप्टिमाइज़ेशन टिप्स के पीछे
ऑप्टिमाइज़ेशन तकनीक "प्रैक्टिस" भाग का हिस्सा हैं, इसलिए इससे पहले कि हम ऐसा करें, आइए ऑप्टिमाइज़ेशन के "थ्योरी" के बारे में बात करें।
परफॉर्मेंस ऑप्टिमाइज़ेशन विधियाँ LuaJIT और OpenResty के इटरेशन के साथ बदलती रहेंगी। कुछ विधियाँ सीधे अंडरलाइंग टेक्नोलॉजी द्वारा ऑप्टिमाइज़ की जा सकती हैं और उन्हें सीखने की आवश्यकता नहीं होगी; साथ ही, कुछ नई ऑप्टिमाइज़ेशन तकनीक भी आएंगी। इसलिए, इन ऑप्टिमाइज़ेशन तकनीक के पीछे के स्थिर अवधारणा को समझना सबसे महत्वपूर्ण है।
आइए OpenResty प्रोग्रामिंग में परफॉर्मेंस के बारे में कुछ महत्वपूर्ण विचारों पर नज़र डालें।
थ्योरी 1: अनुरोधों को संक्षिप्त, सरल और तेज़ी से संसाधित करें
OpenResty एक वेब सर्वर है, इसलिए यह अक्सर 1,000+, 10,000+, या यहाँ तक कि 100,000+ क्लाइंट अनुरोधों को एक साथ संसाधित करता है। इसलिए, सर्वोच्च समग्र प्रदर्शन प्राप्त करने के लिए, हमें यह सुनिश्चित करना चाहिए कि व्यक्तिगत अनुरोधों को तेज़ी से संसाधित किया जाए और विभिन्न संसाधन, जैसे मेमोरी, को पुनः प्राप्त किया जाए।
- यहाँ "संक्षिप्त" का अर्थ है कि अनुरोध जीवनचक्र छोटा होना चाहिए ताकि संसाधनों को लंबे समय तक बिना रिलीज़ किए न रखा जाए; यहाँ तक कि लंबे कनेक्शन के लिए भी, समय या अनुरोधों की संख्या की एक सीमा निर्धारित करनी चाहिए ताकि संसाधनों को नियमित रूप से रिलीज़ किया जा सके।
- दूसरा "सरल" का अर्थ है कि एक API में केवल एक काम करें। जटिल बिजनेस लॉजिक को कई APIs में विभाजित करें और कोड को सरल रखें।
- अंत में, "तेज़" का अर्थ है कि मुख्य थ्रेड को ब्लॉक न करें और बहुत अधिक CPU ऑपरेशन न चलाएं। यदि आपको ऐसा करना ही है, तो पिछले लेख में हमने जिन अन्य विधियों का परिचय दिया था, उनके साथ काम करना न भूलें।
यह आर्किटेक्चरल विचार न केवल OpenResty के लिए उपयुक्त है, बल्कि आगे के विकास भाषाओं और प्लेटफॉर्म के लिए भी है, इसलिए मुझे आशा है कि आप इसे समझेंगे और इसके बारे में सोचेंगे।
थ्योरी 2: मध्यवर्ती डेटा उत्पन्न करने से बचें
मध्यवर्ती प्रक्रिया में अनुपयोगी डेटा से बचना OpenResty प्रोग्रामिंग में सबसे प्रमुख ऑप्टिमाइज़ेशन थ्योरी है। आइए एक छोटे उदाहरण के माध्यम से मध्यवर्ती प्रक्रिया में अनुपयोगी डेटा को समझें।
$ resty -e 'local s= "hello" s = s .. " world" s = s .. "!" print(s) '
इस कोड स्निपेट में, हमने s वेरिएबल पर कई स्प्लिसिंग ऑपरेशन किए ताकि परिणाम hello world! प्राप्त हो। लेकिन केवल अंतिम hello world! स्थिति ही उपयोगी है। s का प्रारंभिक मान और मध्यवर्ती असाइनमेंट सभी मध्यवर्ती डेटा हैं जिन्हें कम से कम उत्पन्न करना चाहिए।
इसका कारण यह है कि ये अस्थायी डेटा इनिशियलाइज़ेशन और GC प्रदर्शन हानि लाते हैं। इन हानियों को कम न समझें; यदि यह लूप जैसे हॉट कोड में दिखाई देता है, तो प्रदर्शन स्पष्ट रूप से कम हो जाएगा। मैं इसे बाद में एक स्ट्रिंग उदाहरण के साथ समझाऊंगा।
string अपरिवर्तनीय हैं
अब, इस लेख के विषय पर वापस आते हैं, string। यहाँ, मैं इस तथ्य पर प्रकाश डाल रहा हूँ कि Lua में string अपरिवर्तनीय हैं।
बेशक, इसका मतलब यह नहीं है कि string को स्प्लिस, मॉडिफाई आदि नहीं किया जा सकता, लेकिन जब हम एक string को मॉडिफाई करते हैं, तो हम मूल string को नहीं बदलते बल्कि एक नया string ऑब्जेक्ट बनाते हैं और string के रेफरेंस को बदलते हैं। इसलिए स्वाभाविक रूप से, यदि मूल string का कोई अन्य रेफरेंस नहीं है, तो इसे Lua के GC (गार्बेज कलेक्शन) द्वारा रिकवर कर लिया जाएगा।
अपरिवर्तनीय string का स्पष्ट लाभ यह है कि यह मेमोरी बचाता है। इस तरह, मेमोरी में एक ही string की केवल एक कॉपी होगी, और अलग-अलग वेरिएबल एक ही मेमोरी एड्रेस की ओर इशारा करेंगे।
इस डिज़ाइन का नुकसान यह है कि जब string जोड़ने और रिक्लेम करने की बात आती है, तो हर बार जब आप एक string जोड़ते हैं, LuaJIT को lj_str_new को कॉल करना पड़ता है ताकि पता लगाया जा सके कि क्या string पहले से मौजूद है; यदि नहीं, तो एक नया string बनाना पड़ता है। यदि आप इसे बहुत बार करते हैं, तो यह प्रदर्शन पर बहुत बड़ा प्रभाव डालेगा।
आइए इस उदाहरण में दिए गए string स्प्लिसिंग ऑपरेशन का एक ठोस उदाहरण देखें, जो कई OpenResty ओपन सोर्स प्रोजेक्ट्स में पाया जाता है।
$ resty -e 'local begin = ngx.now() local s = "" -- `for` लूप, `..` का उपयोग करके स्ट्रिंग स्प्लिसिंग करें for i = 1, 100000 do s = s .. "a" end ngx.update_time() print(ngx.now() - begin) '
यह सैंपल कोड s वेरिएबल पर 100,000 बार string स्प्लिसिंग करता है और रनटाइम प्रिंट करता है। हालांकि उदाहरण थोड़ा चरम है, लेकिन यह परफॉर्मेंस ऑप्टिमाइज़ेशन से पहले और बाद के अंतर को अच्छी तरह से दिखाता है। ऑप्टिमाइज़ेशन के बिना, यह कोड मेरे लैपटॉप पर 0.4 सेकंड में चलता है, जो अभी भी अपेक्षाकृत धीमा है। तो हमें इसे कैसे ऑप्टिमाइज़ करना चाहिए?
पिछले लेखों में, उत्तर दिया गया था, जो कि table का उपयोग करके एक परत का एनकैप्सुलेशन करना है, सभी अस्थायी मध्यवर्ती string को हटाना और केवल मूल डेटा और अंतिम परिणाम को रखना। आइए ठोस कोड इम्प्लीमेंटेशन देखें।
$ resty -e 'local begin = ngx.now() local t = {} -- for लूप जो स्ट्रिंग को रखने के लिए एक ऐरे का उपयोग करता है, हर बार ऐरे की लंबाई गिनता है for i = 1, 100000 do t[#t + 1] = "a" end -- ऐरे के concat मेथड का उपयोग करके स्ट्रिंग्स को जोड़ें local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
हम देख सकते हैं कि यह कोड प्रत्येक स्ट्रिंग को table में बारी-बारी से सेव करता है, और इंडेक्स #t + 1 द्वारा निर्धारित किया जाता है, यानी table की वर्तमान लंबाई प्लस 1। अंत में, table.concat फ़ंक्शन का उपयोग करके प्रत्येक ऐरे एलिमेंट को जोड़ा जाता है। यह स्वाभाविक रूप से सभी अस्थायी स्ट्रिंग्स को छोड़ देता है और 100,000 बार lj_str_new और GC से बचता है।
यह हमारा कोड विश्लेषण था, लेकिन ऑप्टिमाइज़ेशन कैसे काम करता है? ऑप्टिमाइज़ किया गया कोड केवल 0.007 सेकंड लेता है, जो 50 गुना से अधिक प्रदर्शन सुधार दिखाता है। एक वास्तविक प्रोजेक्ट में, प्रदर्शन सुधार और भी अधिक स्पष्ट हो सकता है क्योंकि इस उदाहरण में हमने केवल एक बार में एक कैरेक्टर a जोड़ा है।
यदि नया string 10x a की लंबाई में होता है, तो प्रदर्शन अंतर क्या होगा?
क्या 0.007 सेकंड का कोड हमारे ऑप्टिमाइज़ेशन कार्य के लिए पर्याप्त है? नहीं, इसे और भी ऑप्टिमाइज़ किया जा सकता है। आइए एक और लाइन कोड को संशोधित करें और परिणाम देखें।
$ resty -e 'local begin = ngx.now() local t = {} -- for लूप, स्ट्रिंग को रखने के लिए एक ऐरे का उपयोग करें, ऐरे की लंबाई को स्वयं बनाए रखें for i = 1, 100000 do t[i] = "a" end local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
इस बार, हमने t[#t + 1] = "a" को t[i] = "a" में बदल दिया, और केवल एक लाइन कोड से, हम ऐरे की लंबाई प्राप्त करने के लिए 100,000 फ़ंक्शन कॉल से बच सकते हैं। क्या आपको table सेक्शन में पहले बताया गया ऐरे की लंबाई प्राप्त करने का ऑपरेशन याद है? इसकी टाइम कॉम्प्लेक्सिटी O(n) है, जो एक अपेक्षाकृत महंगा ऑपरेशन है। इसलिए, यहाँ हम केवल अपने ऐरे इंडेक्स को बनाए रखकर ऐरे की लंबाई प्राप्त करने के ऑपरेशन को बायपास करते हैं। जैसा कि कहा जाता है, यदि आप इसे संभाल नहीं सकते, तो इसे टाल दें।
बेशक, यह एक सरल तरीका है। निम्नलिखित कोड और अधिक स्पष्ट रूप से दिखाता है कि कैसे हम स्वयं ऐरे के इंडेक्स को बनाए रख सकते हैं।
$ resty -e 'local begin = ngx.now() local t = {} local index = 1 for i = 1, 100000 do t[index] = "a" index = index + 1 end local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
अन्य अस्थायी string को कम करें
जिन गलतियों के बारे में हमने अभी बात की, string स्प्लिसिंग के कारण अस्थायी string, स्पष्ट हैं। ऊपर दिए गए सैंपल कोड के कुछ रिमाइंडर के साथ, मुझे विश्वास है कि हम फिर से इसी तरह की गलतियाँ नहीं करेंगे। हालांकि, OpenResty में कुछ और छिपे हुए अस्थायी string उत्पन्न होते हैं, जिनका पता लगाना बहुत कम आसान होता है। उदाहरण के लिए, नीचे हम जिस string हैंडलिंग फ़ंक्शन के बारे में बात करेंगे, वह अक्सर उपयोग किया जाता है। क्या आप कल्पना कर सकते हैं कि यह भी अस्थायी string उत्पन्न करता है?
जैसा कि हम जानते हैं, string.sub फ़ंक्शन एक string के निर्दिष्ट भाग को इंटरसेप्ट करता है। जैसा कि हमने पहले बताया, Lua में string अपरिवर्तनीय हैं, इसलिए एक नई स्ट्रिंग को इंटरसेप्ट करने में lj_str_new और बाद के GC ऑपरेशन शामिल होते हैं।
resty -e 'print(string.sub("abcd", 1, 1))'
उपरोक्त कोड का कार्य string के पहले कैरेक्टर को प्राप्त करना और उसे प्रिंट करना है। स्वाभाविक रूप से, यह एक अस्थायी string उत्पन्न करेगा। क्या इसी प्रभाव को प्राप्त करने का एक बेहतर तरीका है?
resty -e 'print(string.char(string.byte("abcd")))'
स्वाभाविक रूप से हाँ। इस कोड को देखते हुए, हम पहले string.byte का उपयोग करके पहले कैरेक्टर का न्यूमेरिक कोड प्राप्त करते हैं और फिर string.char का उपयोग करके नंबर को संबंधित कैरेक्टर में बदलते हैं। इस प्रक्रिया में कोई अस्थायी string उत्पन्न नहीं होता है। इसलिए, string-संबंधित स्कैनिंग और विश्लेषण करने के लिए string.byte का उपयोग करना सबसे कुशल है।
table प्रकार के लिए SDK समर्थन का लाभ उठाएं
अस्थायी string को कम करने का तरीका सीखने के बाद, क्या आप इसे आज़माने के लिए उत्सुक हैं? तो, हम ऊपर दिए गए सैंपल कोड के परिणाम को क्लाइंट को रिस्पॉन्स बॉडी के कंटेंट के रूप में आउटपुट कर सकते हैं। इस बिंदु पर, आप रुक सकते हैं और पहले इस कोड को स्वयं लिखने का प्रयास कर सकते हैं।
$ resty -e 'local begin = ngx.now() local t = {} local index = 1 for i = 1, 100000 do t[index] = "a" index = index + 1 end local response = table.concat(t, "") ngx.say(response) '
यदि आप यह कोड लिख सकते हैं, तो आप अधिकांश OpenResty डेवलपर्स से आगे हैं। OpenResty के Lua API ने पहले ही string स्प्लिसिंग के लिए table के उपयोग को ध्यान में रखा है, इसलिए ngx.say, ngx.print, ngx.log, cosocket:send, और अन्य API जो बहुत सारे string ले सकते हैं, में यह न केवल string को पैरामीटर के रूप में स्वीकार करता है, बल्कि table को भी पैरामीटर के रूप में स्वीकार करता है।
resty -e 'local begin = ngx.now() local t = {} local index = 1 for i = 1, 100000 do t[index] = "a" index = index + 1 end ngx.say(t) '
इस अंतिम कोड स्निपेट में, हमने local response = table.concat(t, ""), string स्प्लिसिंग स्टेप को छोड़ दिया है और table को सीधे ngx.say में पास कर दिया है। यह string स्प्लिसिंग टास्क को Lua स्तर से C स्तर पर शिफ्ट करता है, एक और string लुकअप, जनरेशन और GC से बचाता है। लंबे string के लिए, यह एक और महत्वपूर्ण प्रदर्शन लाभ है।
सारांश
इस लेख को पढ़ने के बाद, हम देख सकते हैं कि OpenResty का बहुत सारा परफॉर्मेंस ऑप्टिमाइज़ेशन विभिन्न विवरणों से संबंधित है। इसलिए, हमें LuaJIT और OpenResty के Lua API को अच्छी तरह से जानने की आवश्यकता है ताकि इष्टतम प्रदर्शन प्राप्त किया जा सके। यह हमें यह भी याद दिलाता है कि यदि हम पिछले कंटेंट को भूल गए हैं, तो हमें समय पर उसकी समीक्षा और समेकन करना चाहिए।
अंत में, एक समस्या के बारे में सोचें: स्ट्रिंग्स hello, world, और ! को एरर लॉग में लिखें। क्या हम string स्प्लिसिंग के बिना एक सैंपल कोड लिख सकते हैं?
साथ ही, लेख में दिए गए अन्य प्रश्न को न भूलें। यदि नया string 10x a की लंबाई में होता है, तो निम्नलिखित कोड में प्रदर्शन अंतर क्या होगा?
$ resty -e 'local begin = ngx.now() local t = {} for i = 1, 100000 do t[#t + 1] = "a" end local s = table.concat(t, "") ngx.update_time() print(ngx.now() - begin) '
आप इस लेख को अपने दोस्तों के साथ सीखने और संवाद करने के लिए भी साझा कर सकते हैं।