Lua/C++ परस्परसंयोजनेचे अनुकूलन

परिचय
Roblox इंजिन हे C++ आणि Lua यांच्या संयोगात लिहिलेले आहे, ज्यात गणनात्मकदृष्ट्या तीव्र ऑपरेशन्स करणारा कोड ऑप्टिमाइझ्ड C++ मध्ये लिहिला जातो, तर गेम लॉजिक आणि स्क्रिप्ट्स विकास सुलभतेसाठी Lua मध्ये लिहिल्या जातात. या मॉडेलचे प्रभावीपणे कार्य करण्यासाठी Lua आणि C++ मधील संक्रमण शक्य तितके जलद असणे आवश्यक आहे, कारण या 'नो मॅन्स लँड'मध्ये घालवलेला प्रत्येक क्षण प्रत्यक्षात वाया गेलेल्या मिलीसेकंदांसारखा असतो.
गेल्या काही महिन्यांत, आम्ही या प्रणालीच्या या भागात विविध सुधारणा राबवत आहोत. विशेषतः एक भाग—Lua मधून C++ मेथड्सचे प्रत्यक्ष कॉल—अत्यंत मनोरंजक ठरला, कारण त्यामुळे लक्षणीय गती सुधारणा झाल्या आणि अंतर्गत कार्यप्रणाली कशी चालते हे समजून घेण्यासाठी Lua च्या आतल्या घटकांमध्ये खोदखोड करावी लागली.
शेवटी आम्ही Lua VM मध्येच बदल केले, परंतु त्याकडे वळण्यापूर्वी काही पायाभूत काम करणे आवश्यक आहे.
कंपाइलर, VM आणि बाइटकोड
जेव्हा Lua स्त्रोत कोड संकलित केला जातो, तेव्हा तो Lua बाइटकोडमध्ये संकलित होतो, जो नंतर Lua VM चालवतो. Lua बाइटकोडमध्ये एकूण सुमारे 35 आज्ञा असतात, जसे की टेबल वाचणे/लिहिणे, फंक्शन्स कॉल करणे, द्विघात ऑपरेशन्स करणे, जंप आणि कंडिशनल इत्यादी. Lua VM हे इतर अनेक VM प्रमाणे स्टॅक-आधारित नसून रजिस्टर-आधारित आहे, त्यामुळे बाइटकोड तयार करताना संकलक प्रत्येक निर्देशासाठी कोणते रजिस्टर वापरायचे हे ठरवतो.
प्रत्येक निर्देशाचे स्वरूप "OP_CODE A B," किंवा "OP_CODE A B C" असे असते, जिथे "OP_CODE" हा ऑपकोड असतो (उदाहरणार्थ, फंक्शन कॉल करण्यासाठी CALL) आणि A/B/C हे ऑपकोडचे आर्गुमेंट्स असतात. हे आर्गुमेंट्स (किंवा रजिस्टर्स) प्रत्यक्ष मूल्ये नसतात. त्याऐवजी, ते दोन टेबलांपैकी एखाद्या टेबलमध्ये निर्देश करणारे निर्देशांक असतात: स्थिर टेबल (Kst(..)) किंवा रजिस्टर टेबल (R(..)).
Lua बाइटकोडचे सविस्तर वर्णन पाहण्यासाठी "A No-Frills introduction to Lua 5.1 VM Instructions" हे पहा. हे ऐकायला जितके साधे वाटते, त्यापेक्षा खूपच रोमांचक आहे; मी वचन देतो!
Lua बाइटकोड कसा दिसतो याची कल्पना देण्यासाठी, आपण प्रथम काही सोपे प्रोग्राम पाहणार आहोत आणि नंतर अधिक संबंधित उदाहरणांकडे पुढे जाऊ.
Chunkspy युटिलिटीचा वापर करून, आपण Lua बाइटकोडचे Lua असेंब्लीमध्ये विघटन करू शकतो आणि कोडची यादी तसेच स्थिर टेबल मिळवू शकतो, ज्यामुळे कोणत्याही Lua स्रोत कोडसाठी कोणता बाइटकोड तयार होतो हे आपण पाहू शकतो.
मूलभूत बाइटकोड उदाहरणे
"x = 10" सारखा साधा प्रोग्राम संकलित केल्यावर:
.const "x"; 0
.const 10; 1
[1] loadk 0 1 ; 10
[2] setglobal 0 0 ; x पहिल्या दोन ओळींमध्ये स्थिर सारणी (slot 0 मध्ये "x" हा स्ट्रिंग मूल्य आणि slot 1 मध्ये 10 हे पूर्णांक मूल्य) दाखवली आहे, आणि पुढील दोन ओळी डिस्असेम्बल केलेल्या ऑपकोड्स आहेत.
[ओळ 1] "No Frills" मध्ये LOADK ऑपकोड शोधल्यावर, आपल्याला त्याचा स्वरूप "LOADK A Bx --- R(A) := Kst(Bx)" असल्याचे दिसते. म्हणून, LOADK चे दोन आर्गुमेंट्स (रजिस्टर A आणि B) आहेत आणि त्याची क्रिया म्हणजे दुसऱ्या रजिस्टरद्वारे दिलेल्या स्लॉटमधील स्थिर टेबलमधील मूल्य पहिल्या आर्गुमेंटद्वारे दिलेल्या स्लॉटमधील रजिस्टर टेबलमध्ये नियुक्त करणे, म्हणजेच Kst(Bx) स्लॉटमधील मूल्य R(A) स्लॉटमध्ये ठेवणे. "Bx" याचा अर्थ असा की ऑपकोडमध्ये फक्त दोन आर्गुमेंट्स असल्यामुळे B रजिस्टर विस्तारित करून त्याला अधिक बिट्स दिले जातात.
[ओळ 2] SETGLOBAL चे स्वरूप "SETGLOBAL A Bx --- Gbl[Kst(Bx)] := R(A)." असे आहे. हे दुसऱ्या अर्गुमेंटच्या स्लॉटमधील स्थिर टेबलने दिलेल्या कीचा वापर करून ग्लोबल टेबलमध्ये एक मूल्य नियुक्त करते. कारण दुसरा अर्गुमेंट 0 आहे आणि 0 च्या स्थिर टेबलमधील मूल्य "x" आहे, त्यामुळे ते "x" कीचा वापर करून ग्लोबल टेबलमध्ये काहीतरी लिहिते. पहिल्या अर्गुमेंटने दिलेल्या स्लॉटमधील रजिस्टर टेबलमध्ये जे काही आहे तेच लिहिले जाते, जे मागील निर्देशने 10 या मूल्याने लोड केले होते.
चला थोडं अधिक गुंतागुंतीचं उदाहरण पाहूया, "x = 10; y = x." कोडचे मॅन्युअल अंमलबजावणी वाचकासाठी एक व्यायाम म्हणून सोडतो. :)
.const "x"; 0
.const 10; 1
.const"y"; 2
[1] loadk 0 1 ; 10
[2] setglobal 0 0 ; x
[3] getglobal 0 0 ; x
[4] setglobal 0 2 ; y
फंक्शन कॉलसाठी बाइटकोड
"foo(10):" साठी तयार झालेल्या कोडवर एक नजर टाकूया:
.const "foo"; 0
.const 10; 1
[1] getglobal 0 0 ; foo // R(A) := Gbl[Kst(Bx)]
[2] loadk 1 1 ; 10 // R(A) := Kst(Bx)
[3] call 0 2 1 फंक्शन
कॉल अंमलात आणण्यासाठी, फंक्शन पहिल्या रजिस्टरमध्ये आणि आर्गुमेंट्स पुढील रजिस्टरमध्ये लोड केले पाहिजेत. "CALL A B C" चे अर्थशास्त्र असे आहे की A मध्ये फंक्शन असते, B मध्ये आर्गुमेंट्सची संख्या असते (प्रत्यक्षात, "..." ची अंमलबजावणी लक्षात घेता ही संख्या आर्गुमेंट्सची संख्या +1 असते), आणि C मध्ये परताव्याच्या मूल्यांची संख्या असते (पुन्हा, एकापेक्षा जास्त परताव्याच्या मूल्यांसाठी ही संख्या परताव्याच्या मूल्यांची संख्या +1 असते).
आपण पहिल्या दोन ओळींशी परिचित आहोत; त्या register table स्लॉट 0 मध्ये एक मूल्य लोड करतात आणि register table स्लॉट 1 मध्ये 10 हे मूल्य लोड करतात. तिसरी ओळ फंक्शन कॉल करते, ज्यात रजिस्टर A (रजिस्टर टेबल स्लॉट 0, ज्यात "foo" लोड केले गेले होते) मधील मूल्य वापरले जाते, B मध्ये आर्गुमेंट्सची संख्या आणि C मध्ये रिटर्न व्हॅल्यूजची संख्या निर्दिष्ट केली जाते (लक्षात ठेवा, B आणि C दोन्ही मूल्यांमध्ये 1 जोडले पाहिजे). फंक्शन कॉल करण्यापूर्वी, VM R(A) मधील मूल्य खरोखर कॉल करण्यायोग्य आहे का हे देखील पडताळते.
Lua कडे एक यंत्रणा आहे जी वापरकर्त्यांना विद्यमान टेबलशी मेटाटेबल जोडल्यानंतर टेबलची कार्यक्षमता वाढवण्याची परवानगी देते. मेटाटेबलमध्ये पर्यायी पद्धती (fallback methods) असतात ज्या मुख्य टेबलवर काही विशिष्ट पद्धत किंवा ऑपरेशन करता येत नसल्यास चालवल्या जातात (सविस्तर वर्णनासाठी https://www.lua.org/pil/13.html पहा).
आमच्या हेतूंसाठी, मेटाटेबलमधील सर्वात संबंधित नोंदी म्हणजे "__index" आणि "__call" फील्ड्स. __index चा वापर टेबलमध्ये एखादा घटक शोधताना केला जातो, त्यामुळे "local x = my_table[10]" हा कोड प्रथम my_table वरील __index पद्धतीला कॉल करेल. जर ते अयशस्वी ठरले, तर त्याऐवजी ते my_table च्या मेटाटेबलवरील __index पद्धतीला कॉल करण्याचा प्रयत्न करेल. __call हे देखील त्याचप्रमाणे वापरले जाते जेव्हा आपण एखाद्या गोष्टीला फंक्शन म्हणून वागवून त्याला कॉल करण्याचा प्रयत्न करतो, उदाहरणार्थ "local x = foo(42)"
Lua आणि C++ यांना परस्परसंवाद साधण्यासाठी, त्यांना फंक्शन्स आणि डेटा शेअर करण्याचा काही मार्ग आवश्यक असतो. Lua हे UserData नावाच्या डेटा प्रकाराद्वारे हे सुलभ करते. UserData ऑब्जेक्ट्स C++ मध्ये तयार करता येतात, आणि ते मूळ Lua डेटा प्रकार असल्यामुळे, त्यांना मेटाटेबल्सने सजवता येते ज्यामुळे Lua कोड त्यांना जणू साध्या Lua ऑब्जेक्ट्सप्रमाणेच वापरू शकतो.
सदस्य फंक्शन कॉल
ठीक आहे, आता पुन्हा काही बाइटकोड पाहूया! पुढील उदाहरण थोडं अधिक मनोरंजक आहे कारण ते दाखवते की जेव्हा तुमच्याकडे "foo:bar(10)" सारखा कोड असतो, जो foo या उदाहरणावर (Foo वर्गाचं उदाहरण) bar मेथड कॉल करतो.
foo:bar(10)
.const "foo"; 0
.const "bar"; 1
.const 10; 2
[1] getglobal 0 0 ; foo
[2] self 0 0 257 ; "bar"
[3] loadk 2 2 ; 10
[4] call 0 3 1 येथे नवीन
गोष्ट म्हणजे self इंस्ट्रक्शन [ओळ 2], जी आपण आधी पाहिली नाही. Self ची सिंटॅक्स अशी आहे: "SELF A B C --- R(A) := R(B)[RK(C)]; R(A+1) := R(B)," तर चला याचे विघटन करूया. रजिस्टर टेबलमध्ये स्लॉट R(A) मध्ये, स्लॉट RK(C) मधील की वापरून टेबलमधील स्लॉट R(B) मधील मूल्य शोधण्याचा निकाल R(B) मध्ये ठेवला जाईल. तसेच स्लॉट R(B) मधील जे काही आहे ते स्लॉट R(A+1) मध्ये कॉपी केले जाईल, परंतु याबद्दल पुढे अधिक. तुम्हाला लक्षात येईल की C रजिस्टरचे मूल्य 257 आहे. हे वैध आहे कारण Lua मूल्य शोधण्यासाठी RK(C) वापरत आहे, आणि RK 9व्या बिटच्या मूल्यानुसार रजिस्टर टेबल किंवा स्थिरांक टेबलपैकी एक वापरते. जर ते 1 असेल, जसे या प्रकरणात आहे, तर स्थिरांक टेबल वापरले जाते; अन्यथा, सर्वात उच्च बिट मास्क केल्यानंतर शोध रजिस्टर टेबलमध्ये होतो.
ओळ 3 स्लॉट 2 मध्ये 10 ठेवते, आणि शेवटी ओळ 4 फंक्शन कॉल अंमलात आणेल.
SELF निर्देशाचे दोन हेतू आहेत. प्रथम, तो Foo वर्गावरील "bar" पद्धत शोधतो आणि ती R(A) मध्ये ठेवतो. दुसरे म्हणजे, foo ही एक इन्स्टन्स पद्धत असल्यामुळे आणि कॉल करताना ज्या वर्गावर आपण पद्धत चालवत आहोत त्या वर्गाची इन्स्टन्स आवश्यक असल्यामुळे, ती ही इन्स्टन्स R(A+1) मध्ये ठेवते. जर तुम्हाला Python मधील वर्गांची माहिती असेल, तर तुम्हाला ही संकल्पना ओळखीची वाटेल: पद्धती सहसा "def my_method(self, arg1, arg2..)" अशा स्वरूपात लिहिल्या जातात, जिथे self हा वर्गाचा इन्स्टन्स असतो.
आपल्याला यात थोडे खोलवर जावे लागेल आणि पाहावे लागेल की जेव्हा foo उदाहरण C++ ऑब्जेक्ट असते आणि ते Lua मध्ये UserData ऑब्जेक्ट म्हणून प्रतिनिधित्व केले जाते तेव्हा काय होते.
SELF कॉलला टेबल लुकअपप्रमाणे पाहता येते, म्हणजेच Foo["bar"] (मोठ्या अक्षरातील Foo हा वर्ग Foo दर्शवतो, foo हा उदाहरण नव्हे), आणि आपल्याला माहित आहे की लुकअप्स __index पद्धत वापरतात. जेव्हा foo उदाहरण C++ मध्ये तयार केले गेले, तेव्हा त्या उदाहरणाशी एक मेटाटेबल जोडले गेले, आणि त्या मेटाटेबलच्या __index फील्डमध्ये असा C++ कोड सेट केला गेला की जेव्हा __index कॉल होईल तेव्हा तो कोड चालवला जाईल.
जेव्हा Lua मधून C/C++ कॉल केला जातो, तेव्हा फक्त एक lua_State ऑब्जेक्ट पास केला जातो. या ऑब्जेक्टमध्ये सध्या चालू असलेल्या Lua थ्रेडशी संबंधित सर्वकाही असते. state ऑब्जेक्टमधील सर्वात महत्त्वाची माहिती म्हणजे Lua स्टॅक, ज्यात फंक्शनचे आर्गुमेंट्स असतात (lua_tointeger/tostring इत्यादी फंक्शन्सच्या कुटुंबाद्वारे प्रवेश) आणि ते Lua कडे मूल्ये परत करण्यासाठी देखील वापरले जाते.
प्यूडो-C++ मध्ये, आमचे __index फंक्शन काहीसं असे दिसते:
int metaIndex(lua_State* L)
{
// first argument is the userdata object
UserData* userdata = lua_touserdata(L, 1);
// get some kind of descriptor, that contains information
// about what methods the class exposes
ClassDescriptor* desc = getDescriptorForUserData(userdata);
// See if the class has the requested method
const char* methodName = lua_tostring(L, 2);
MemberFunctionPtr method = desc->hasMethod(methodName);
if (method)
{
// Upvalues are values that are available when a C
// function is invoked.
lua_pushupvalue(L, method);
lua_pushcfunction(L, methodInvoker);
return 1;
}
else
{
lua_pushnil(L);
return 0;
}
}
अनेक अंतर्गत गोष्टी सोप्या पद्धतीने मांडल्या आहेत, परंतु त्याचा सार असा आहे. Lua स्टॅकवर पहिल्या आर्गुमेंट म्हणून पास केलेला UserData ऑब्जेक्ट मिळाल्यावर, आम्ही प्रत्यक्ष C++ क्लासचे वर्णन करणारा एक डिस्क्रिप्टर शोधू शकतो, आणि त्या डिस्क्रिप्टरद्वारे आम्ही पाहू शकतो की या क्लासमध्ये दिलेल्या नावाची मेथड आहे का. जर ती असेल, तर मेथड इन्व्होकरचा फंक्शन पॉइंटर Lua स्टॅकवर पुश केला जातो, आणि आम्ही यश परत करतो.
या कॉलनंतर, Lua VM उर्वरित सर्व आर्गुमेंट्स रजिस्टर टेबलमध्ये ठेवेल, आणि नंतर आम्ही मेटाइंडेक्स पद्धतीतून परत दिलेल्या फंक्शनला कॉल करेल, जे पुन्हा C++ मध्ये कॉल करेल आणि इन्वोक फंक्शनमध्ये पोहोचेल:
int methodInvoker(lua_State* L)
{ <br> // Get the userdata and the class descriptor
UserData* userdata = lua_touserdata(L, 1);
ClassDescriptor* desc = getDescriptorForUserData(userdata);
Class* instance = (Class*)userdata;
// Using Lua's upvalue mechanism, get the 'method'
// that was stored in metaIndex.
MemberFunctionPtr method = lua_getupvalue(L, 1);
// This is hand-wavey, but we have some mechanism of being
// able to invoke a member function via the class descriptor,
// and also pop arguments from the Lua stack, and push return values
return desc->invokeFunction(instance, method, L);
}
methodInvoker देखील ClassDescriptor वापरतो, परंतु यावेळी तो सदस्य फंक्शन कॉल करू शकतो आणि स्टॅकवरून योग्य आर्गुमेंट्स काढू शकतो.
शेवटचा टप्पा!
आता आपण Lua ते C++ या दोन फेरींचे स्पष्टपणे पाहू शकतो, त्यामुळे आपण हे कसे ऑप्टिमाइझ करायचे ते शोधू शकतो.
आमचे अंतिम उद्दिष्ट म्हणजे Lua कडून C++ कडे एकच फंक्शन कॉल करणे आणि पद्धत शोधणे व कॉल करणे एकाच वेळी करता यावेत म्हणून आवश्यक सर्व घटक Lua स्टॅकवर असणे. समस्या अशी दिसते की आपल्याकडे एक रजिस्टर कमी आहे. जेव्हा आपण आपले एकत्रित lookup/invoker फंक्शन कॉल करतो, तेव्हा आपण Lua स्टॅक [self, method name, arg1, arg2, ...] असा दिसावा अशी अपेक्षा करतो, परंतु SELF पाहिल्यावर आपल्याला आढळते की ते पद्धत फंक्शन शोधण्याच्या निकालासाठी त्याचा पहिला स्लॉट आणि इन्स्टन्स साठवण्यासाठी दुसरा स्लॉट वापरते.
__call मेटा-मेथड कसा काम करतो हे पाहिल्यावर एक महत्त्वाची जाणीव झाली. जर एखाद्या ऑब्जेक्टमध्ये __call मेटा-मेथड असेल, तर _call फंक्शन कॉल होण्यापूर्वी तो ऑब्जेक्ट स्वतः स्टॅकवर पुश होतो आणि सर्व आर्गुमेंट्स वर सरकवले जातात. या कार्यक्षमतेचा फायदा घेऊन, "self" स्पष्टपणे रजिस्टरमध्ये संग्रहित न करता स्टॅकवर मिळवण्याचा एक मार्ग होता.
दुसऱ्या भागात मेथडचे नाव देखील स्टॅकवर आणणे आवश्यक होते. यासाठी आम्हाला SELF ऑपकोडच्या कार्यप्रणालीत हुशारीने बदल करावा लागला.
लक्षात ठेवा की डीफॉल्ट प्रकरणात, SELF सदस्य फंक्शन शोधण्याचा प्रयत्न करेल आणि ते R(A) मध्ये इन्स्टन्स R(A+1) सोबत संग्रहित करेल. आम्ही संपूर्णपणे शोध प्रक्रिया वगळून प्रत्यक्ष ऑब्जेक्ट R(A) मध्ये आणि मेथडचे नाव R(A+1) मध्ये संग्रहित केले.
जर आपण आता R(A) मधील ऑब्जेक्टमध्ये __call मेटामेथड असल्याची खात्री केली, तर self देखील स्टॅकवर पुश होईल. त्यामुळे, आमच्याकडे [self, method name, args…] असा स्टॅक असेल आणि C++ मध्ये फक्त एकच कॉल होईल. उत्तम! बरं, जवळजवळ. :)
हे पूर्ण झाल्याचा विचार करण्यापूर्वी, आम्हाला त्यावर काही अंतिम सुधारणा करायच्या होत्या. आम्हाला __call मेटामेथडच्या अर्थावर ओव्हरलोड करायचे नव्हते, म्हणून त्याऐवजी आम्ही या प्रकारच्या कॉलसाठी __namecall नावाची एक विशिष्ट मेटामेथड जोडली, जी फक्त UserData ऑब्जेक्टवरच उपलब्ध होती. आम्ही SELF ओपकोडमध्येही बदल केला, ज्यामुळे ऑब्जेक्टमध्ये __namecall मेटामेथड असल्यासच तो नवीन अर्थ वापरतो.
आम्ही केलेली दुसरी गोष्ट म्हणजे नवीन मार्ग आणि जुना मार्ग यांना कोड सहजपणे सामायिक करण्यास सक्षम करणे. पद्धतीचे नाव दुसरा तर्क म्हणून घेण्याऐवजी, आम्ही ते शेवटच्या तर्कात ढकलले. त्यामुळे, पद्धतीचा पॉइंटर शोधण्यासाठी त्याचा वापर झाल्यानंतर, तो स्टॅकवरून सहजपणे काढता येत असे आणि स्टॅक जुन्या मार्गाद्वारे फंक्शन कॉल केल्याप्रमाणे दिसत असे.
निष्कर्ष
या ऑप्टिमायझेशनचा परिणाम किती होतो? बरं, प्रोग्रामिंगमधील बहुतेक गोष्टींप्रमाणे, उत्तर आहे "हे अवलंबून असते." ज्या फंक्शन्स जड असतात—आणि तुम्ही त्यांना वारंवार कॉल करत नाही—त्यासाठी तुम्हाला फारसा सुधारणा दिसेलच नाही. पण तुम्ही वारंवार कॉल करत असलेल्या लहान फंक्शन्ससाठी, बचत लक्षणीय असू शकते.
डेव्हलपर फोरमवरील लोकांनी या विचित्र, नवीन मेटा-मेथडच्या प्रकट होण्याकडे लवकरच लक्ष वेधले, आणि एक तक्ता सादर करण्यात आला ज्यात __namecall च्या गतीची तुलना इंस्टन्स मेथड्स कॉल करण्याच्या जुन्या पद्धतीशी तसेच मेथड कॉल ऑप्टिमाइझ करण्यासाठी डेव्हलपर्स वापरत असलेल्या एका वर्कअराउंडशी करण्यात आली होती:
local part = workspace.Baseplate
local count = 1000000
local start0 = tick()
for i=1,count do
part:IsA("BasePart")
end
local end0 = tick()
local start1 = tick()
for i=1,count do
local isa = part.IsA
isa(part, "BasePart")
end
local end1 = tick()
local start2 = tick()
local isa = part.IsA
for i=1,count do
isa(part, "Basepart")
end
local end2 = tick()
print("namecall", end0 - start0)
print("index+call", end1 - start1)
print("call", end2 - start2)
> namecall 0.49229717254639
> index+call 0.78510332107544
> call 0.49960780143738
पहिला
लूप नवीन __namecall कोड पथ वापरतो, परंतु सर्व जादू पार्श्वभूमीत घडत असल्यामुळे, ऑप्टिमायझेशनचा लाभ घेण्यासाठी विकासकांना कोणताही विद्यमान कोड बदलावा लागणार नाही.
दुसरा लूप इन्स्टन्स मेथड कॉल करण्याची जुनी पद्धत अनुकरण करतो; प्रथम मेथड शोधण्यासाठी लुकअप करतो आणि नंतर त्याला कॉल करतो.
आणि शेवटी, तिसरा लूप एक सामान्य ऑप्टिमायझेशन दाखवतो जे विकासक करत होते, ज्यात पद्धत प्रथम शोधली जात असे, स्थानिक चलमध्ये संग्रहित केली जात असे, आणि नंतर त्या चल्याला कॉल केले जात असे.
येथे चांगली गोष्ट म्हणजे ती दाखवते की __namecall ऑप्टिमायझेशनसह, इंस्टन्स फंक्शन्स स्पष्टपणे कॅश करणे आता आवश्यक नाही, कारण ते कॅश केलेल्या ऑप्टिमायझेशनइतकेच जलद आहे, त्यामुळे सर्वात सरळ कोडही सर्वात कार्यक्षम ठरेल.
आता __namecall लागू झाल्यामुळे आणि आपण पाहत असलेल्या निकालांबद्दल समाधानी असल्यामुळे, आता आपले लक्ष मेमरी वापराकडे वळवण्याची आणि त्या क्षेत्रात क्लायंट सुधारण्यासाठी आपण काय करू शकतो हे पाहण्याची वेळ आली आहे!


