லூவா/சி++ இடைசெயல்பாட்டை மேம்படுத்துதல்

அறிமுகம்
ராப்ளாக்ஸ் இயந்திரம் C++ மற்றும் லூவா ஆகியவற்றின் கலவையில் எழுதப்பட்டுள்ளது. கணக்கீட்டு ரீதியாக அதிக தீவிரம் கொண்ட செயல்பாடுகளைச் செய்யும் குறியீடு உகந்தப்படுத்தப்பட்ட C++-இல் எழுதப்பட்டுள்ளது, அதே நேரத்தில் விளையாட்டு தர்க்கம் மற்றும் ஸ்கிரிப்டுகள் மேம்பாட்டு எளிமைக்காக லூவாவில் எழுதப்பட்டுள்ளன. இந்த மாதிரி பயனுள்ளதாக இருக்க, லூவா மற்றும் C++-க்கு இடையிலான மாற்றங்கள் முடிந்தவரை வேகமாக இருக்க வேண்டும், ஏனெனில் இந்த இடைவெளியில் செலவிடப்படும் எந்த நேரமும் அடிப்படையில் வீணடிக்கப்பட்ட மில்லிவினாடிகளே.
கடந்த சில மாதங்களாக, நாங்கள் இந்த அமைப்பின் இந்தப் பகுதிக்கு பல்வேறு மேம்பாடுகளைச் செய்து வருகிறோம். குறிப்பாக ஒரு பகுதி—லூவாவிலிருந்து C++ முறைகளை உண்மையில் அழைப்பது—மிகவும் சுவாரஸ்யமாக இருந்தது, ஏனெனில் அது குறிப்பிடத்தக்க வேக மேம்பாடுகளுக்கு வழிவகுத்தது மற்றும் உள்ளமைப்பில் விஷயங்கள் எவ்வாறு செயல்படுகின்றன என்பதைப் புரிந்துகொள்ள லூவாவின் உள்ளமைப்பில் ஆழமாக ஆய்வு செய்ய வேண்டியிருந்தது.
நாங்கள் இறுதியில் லூவா VM-ஐயே மாற்றியமைத்தோம், ஆனால் அதற்கு வருவதற்கு முன், நாம் சில அடிப்படைகளை அமைக்க வேண்டும்.
கம்பைலர்கள், VM, மற்றும் பைட்கோடு
லூவா மூலக் குறியீடு தொகுக்கப்படும்போது, அது லூவா பைட்கோடாகத் தொகுக்கப்படுகிறது, அதை லூவா VM பின்னர் இயக்குகிறது. லூவா பைட்கோடில், அட்டவணைகளைப் படிப்பது/எழுதுவது, செயல்பாடுகளை அழைப்பது, இருமக் கணக்கீடுகளைச் செய்வது, ஜம்ப் மற்றும் நிபந்தனைகள் போன்றவற்றிற்காக மொத்தம் சுமார் 35 அறிவுறுத்தல்கள் உள்ளன. பல பிற VM-களைப் போல ஸ்டேக்-அடிப்படையாக இருப்பதற்கு மாறாக, லூவா VM ரெஜிஸ்டர்-அடிப்படையிலானது. எனவே, பைட்கோடை உருவாக்கும்போது தொகுப்பான் செய்யும் பணிகளில் ஒரு பகுதி, ஒவ்வொரு அறிவுறுத்தலும் எந்த ரெஜிஸ்டர்களைப் பயன்படுத்த வேண்டும் என்பதைத் தீர்மானிப்பதாகும்.
ஒவ்வொரு அறிவுறுத்தலும் "OP_CODE A B," அல்லது "OP_CODE A B C" என்ற வடிவத்தில் இருக்கும், இதில் "OP_CODE" என்பது ஆப்கோடு ஆகும் (உதாரணமாக, ஒரு செயல்பாட்டை அழைப்பதற்கான CALL), மற்றும் A/B/C என்பவை ஆப்கோடு வாதங்கள் ஆகும். காரணிகள் (அல்லது பதிவுகள்) உண்மையான மதிப்புகள் அல்ல. மாறாக, அவை இரண்டு அட்டவணைகளில் ஒன்றைக் குறிக்கும் குறியீடுகளாகும்: நிலையான அட்டவணை (Kst(..)) அல்லது பதிவு அட்டவணை (R(..)).
லூவா பைட்கோডের விரிவான விளக்கத்திற்கு, "A No-Frills introduction to Lua 5.1 VM Instructions" என்பதைப் பார்க்கவும். இது கேட்பதற்கு இருப்பதை விட மிகவும் சுவாரஸ்யமானது; நான் உறுதியளிக்கிறேன்!
லூவா பைட்கோடு எப்படி இருக்கும் என்பதைப் பற்றி உங்களுக்கு ஒரு உணர்வைக் கொடுக்க, நாம் முதலில் சில எளிய நிரல்களைப் பார்த்துவிட்டு, பின்னர் மேலும் தொடர்புடைய எடுத்துக்காட்டுகளுக்குச் செல்வோம்.
Chunkspy பயன்பாட்டைப் பயன்படுத்தி, லூவா பைட்கோடை லூவா அசெம்பிளியாக பிரித்தெடுத்து, குறியீட்டின் பட்டியலையும், நிலையான அட்டவணையையும் பெறலாம். இதன் மூலம், எந்தவொரு லூவா மூலக் குறியீட்டிற்கும் என்ன பைட்கோடு உருவாக்கப்படுகிறது என்பதை நம்மால் காண முடியும்.
அடிப்படை பைட்கோட் எடுத்துக்காட்டுகள்
"x = 10" போன்ற ஒரு எளிய நிரல் இவ்வாறு தொகுக்கப்படுகிறது:
.const "x"; 0
.const 10; 1
[1] loadk 0 1 ; 10
[2] setglobal 0 0 ; x முதல் இரண்டு வரிகள் நிலையான அட்டவணையைக் காட்டுகின்றன (இடம் 0-இல் "x" என்ற சர மதிப்பையும், இடம் 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 ஆகும்) ஆகும்.
முதல் இரண்டு வரிகளை நாம் அறிவோம்; அவை பதிவேற்ற அட்டவணை ஸ்லாட் 0-இல் ஒரு மதிப்பையும், பதிவேற்ற அட்டவணை ஸ்லாட் 1-இல் 10 என்ற மதிப்பையும் ஏற்றுகின்றன. மூன்றாவது வரியானது, பதிவு A-இல் (பதிவு அட்டவணை இடம் 0, இதில் "foo" ஏற்றப்பட்டிருந்தது) உள்ள மதிப்பைப் பயன்படுத்தி செயல்பாட்டு அழைப்பைச் செய்கிறது. இதில் B வாதங்களின் எண்ணிக்கையையும், C திருப்பி அனுப்பப்படும் மதிப்புகளின் எண்ணிக்கையையும் குறிக்கிறது (நினைவில் கொள்ளுங்கள், B மற்றும் C ஆகிய இரண்டின் மதிப்புகளிலும் 1 சேர்க்கப்பட்டிருக்க வேண்டும்). செயல்பாட்டை அழைப்பதற்கு முன்பு, VM ஆனது R(A)-இல் உள்ள மதிப்பு உண்மையில் அழைக்கக்கூடியதா என்பதையும் சரிபார்க்கிறது.
ஒரு அட்டவணையுடன் ஒரு மெட்டேபிளையை இணைப்பதன் மூலம், அட்டவணைகளின் செயல்பாட்டை விரிவாக்க பயனர்களை அனுமதிக்கும் ஒரு வழிமுறையை லூவா கொண்டுள்ளது. ஒரு குறிப்பிட்ட முறை அல்லது செயல்பாட்டை முதன்மை அட்டவணையில் செயல்படுத்த முடியாவிட்டால், மெட்டேபிளையில் உள்ள மாற்று முறைகள் அழைக்கப்படும் (விரிவான விளக்கத்திற்கு https://www.lua.org/pil/13.html ஐப் பார்க்கவும்).
நமது தேவைகளுக்கு, மெட்டேபிளிலுள்ள மிகவும் தொடர்புடைய உள்ளீடுகள் "__index" மற்றும் "__call" புலங்களாகும். ஒரு அட்டவணையில் ஒரு உறுப்பைத் தேடும்போது __index பயன்படுத்தப்படுகிறது, எனவே "local x = my_table[10]" என்ற குறியீடு முதலில் my_table-இன் __index முறையை அழைக்கும். அது தோல்வியுற்றால், அதற்குப் பதிலாக my_table-இன் metatable-இல் உள்ள __index-ஐ அழைக்க முயற்சிக்கும். __call-ம் இதேபோல, நீங்கள் ஒன்றை ஒரு செயல்பாடாகக் கருதி அதை அழைக்க முயற்சிக்கும்போது பயன்படுத்தப்படுகிறது, உதாரணமாக "local x = foo(42)" என.
லூவா மற்றும் சி++ ஆகியவை ஒன்றுடன் ஒன்று செயல்படுவதற்கு, அவை செயல்பாடுகளையும் தரவையும் பகிர்ந்து கொள்ள ஒரு வழி தேவை. லூவா, UserData எனப்படும் ஒரு தரவு வகையை வழங்குவதன் மூலம் இதை எளிதாக்குகிறது. சி++ பக்கத்தில் UserData பொருட்களை உருவாக்கலாம், மேலும் அவை பூர்வீக லூவா தரவு வகைகளாக இருப்பதால், அவற்றுடன் மெட்டாடேபிள்களை இணைக்க முடியும். இது, லூவா குறியீடு அவற்றை சாதாரண லூவா பொருட்களைப் போலவே கையாள அனுமதிக்கிறது.
உறுப்பினர் செயல்பாட்டு அழைப்புகள்
சரி, சில பைட்கோடைப் பார்ப்போம்! இந்த அடுத்த எடுத்துக்காட்டு சற்று சுவாரஸ்யமானது, ஏனெனில் இது "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, செயல்பாட்டு அழைப்பை (function call) செயல்படுத்தும்.
SELF அறிவுறுத்தல் இரண்டு நோக்கங்களைக் கொண்டுள்ளது. முதலாவதாக, இது Foo வகுப்பில் உள்ள "bar" முறையைத் தேடி, அதை R(A)-ல் வைக்கிறது. இரண்டாவதாக, foo ஒரு இன்ஸ்டன்ஸ் மெத்தட் என்பதாலும், அழைப்பைச் செய்யும்போது நாம் எந்த வகுப்பின் (class) இன்ஸ்டன்ஸை அழைக்கிறோமோ அந்த இன்ஸ்டன்ஸ் நமக்குத் தேவை என்பதாலும், இந்த இன்ஸ்டன்ஸை R(A+1)-ல் வைக்கிறது. நீங்கள் பைத்தானில் உள்ள வகுப்புகளுடன் (classes) பரிச்சயமாக இருந்தால், இந்தக் கருத்தை நீங்கள் அடையாளம் காணலாம்: மெத்தட்கள் பொதுவாக "def my_method(self, arg1, arg2..)," என எழுதப்படுகின்றன, இதில் self என்பது வகுப்பின் இன்ஸ்டன்ஸ் ஆகும்.
நாம் இதை இன்னும் ஆழமாக ஆராய்ந்து, foo நிகழ்வு ஒரு C++ பொருளாக இருந்து, அது Lua-வில் ஒரு UserData பொருளாகப் பிரதிநிதித்துவப்படுத்தப்படும்போது என்ன நடக்கிறது என்பதைப் பார்க்க வேண்டும்.
SELF அழைப்பை ஒரு அட்டவணைத் தேடலாகக் (table lookup) காணலாம், அதாவது Foo["bar"] (பெரிய எழுத்து Foo என்பது வகுப்பு Foo-வைக் குறிக்கிறது, foo என்பது நிகழ்வைக் குறிக்கிறது), மேலும் தேடல்கள் __index முறையைப் பயன்படுத்தும் என்பது நமக்குத் தெரியும். C++-ல் foo நிகழ்வு உருவாக்கப்பட்டபோது, ஒரு மெட்டா டேபிள் அந்த நிகழ்வுடன் தொடர்புபடுத்தப்பட்டது, மேலும் அந்த மெட்டா டேபிளின் __index புலமானது, __index அழைக்கப்படும்போது அழைக்கப்படும் ஒரு C++ குறியீட்டுத் துண்டிற்கு அமைக்கப்பட்டது.
லூவாவிலிருந்து C/C++ அழைக்கப்படும்போது, அனுப்பப்படும் ஒரே தரவு lua_State என்ற பொருள் மட்டுமே. இந்தப் பொருள், தற்போது இயங்கிக்கொண்டிருக்கும் லூவா திரிக்கு (thread) தொடர்பான அனைத்தையும் கொண்டுள்ளது. state பொருளில் உள்ள மிக முக்கியமான தகவல் லூவா ஸ்டேக் (Lua stack) ஆகும், இது செயல்பாட்டு வாதங்களை (function arguments) (lua_tointeger/tostring போன்ற செயல்பாட்டுக் குடும்பங்கள் வழியாக அணுகப்படுகிறது) கொண்டுள்ளது மற்றும் லூவாவிற்கு மதிப்புகளைத் திரும்ப அனுப்பவும் இது பயன்படுத்தப்படுகிறது.
ப்சூடோ-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;
}
}
பல உள் விவரங்கள் சுருக்கமாகக் கூறப்பட்டுள்ளன, ஆனால் அதன் சாராம்சம் இதுதான். லூவா ஸ்டேக்கில் முதல் வாதமாக அனுப்பப்படும் UserData பொருளைக் கொண்டு, உண்மையான C++ வகுப்பை விவரிக்கும் ஒரு விவரிப்பானை (descriptor) நாம் கண்டறிய முடிகிறது, மேலும் அந்த விவரிப்பான் மூலம் இந்த வகுப்பில் கொடுக்கப்பட்ட பெயரில் ஒரு முறை (method) உள்ளதா என்பதைப் பார்க்க முடியும். அப்படி இருந்தால், ஒரு முறை அழைப்பான் (method invoker)க்கான ஒரு செயல்பாட்டு முனை (function pointer) லூவா ஸ்டேக்கில் தள்ளப்படுகிறது, மேலும் நாங்கள் வெற்றியைத் திரும்பத் தருகிறோம்.
இந்த அழைப்பிற்குப் பிறகு, லூவா VM மீதமுள்ள வாதங்களை ரெஜிஸ்டர் அட்டவணையில் வைக்கும், பின்னர் நாங்கள் metaIndex முறையிலிருந்து திருப்பியளித்த செயல்பாட்டை அழைக்கும், அது மீண்டும் C++-ஐ அழைத்து, அழைப்பாளர் செயல்பாட்டில் (invoker function) செல்லும்:
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-ஐப் பயன்படுத்துகிறது, ஆனால் இந்த முறை அது மெம்பர் ஃபங்ஷனை அழைக்கவும், ஸ்டேக்கிலிருந்து சரியான ஆர்க்யூமென்ட்களைப் பெறவும் முடிகிறது.
இறுதிக்கட்டம்!
இப்போது லூவிலிருந்து சி++-க்கு இரண்டு முறை தகவல்தொடர்பு நடப்பதை நாம் தெளிவாகக் காணலாம், இதை எவ்வாறு மேம்படுத்துவது என்பதைக் கண்டறிய முயற்சிப்போம்.
லூவாவிலிருந்து சி++-க்கு ஒரே ஒரு செயல்பாட்டு அழைப்பைச் செய்து, ஒரே நேரத்தில் முறை தேடல் மற்றும் அழைப்பைச் செய்யத் தேவையான அனைத்து கூறுகளையும் லூவா ஸ்டேக்கில் வைத்திருப்பதே நமது இறுதி இலக்காகும். நமக்கு ஒரு ரெஜிஸ்டர் குறைவாக இருப்பதுதான் பிரச்சனையாகத் தெரிகிறது. நாம் நமது ஒருங்கிணைந்த லுக்அப்/இன்கேக்கர் (lookup/invoker) ஃபங்ஷனை அழைக்கும்போது, லூவா ஸ்டேக் [self, method name, arg1, arg2, ...] போன்ற தோற்றத்தில் இருக்க வேண்டும் என்று நாம் விரும்புகிறோம், ஆனால் SELF-ஐப் பார்க்கும்போது, அது மெத்தட் ஃபங்ஷனைத் தேடுவதற்கான முடிவை அதன் முதல் ஸ்லாட்டிலும், இன்ஸ்டன்ஸைச் சேமிப்பதற்கு இரண்டாவது ஸ்லாட்டிலும் பயன்படுத்துவதைக் காண்கிறோம்.
__call மெட்டாமெத்தட் செயல்படும் விதத்தைப் பார்த்தபோது ஒரு முக்கியமான உண்மை தெரிந்தது. ஒரு ஆப்ஜெக்ட்டில் __call மெட்டாமெத்தட் இருந்தால், _call ஃபங்ஷன் அழைக்கப்படுவதற்கு முன்பு, அந்த ஆப்ஜெக்ட் தானே ஸ்டேக்கில் தள்ளப்படுகிறது மற்றும் அனைத்து ஆர்க்யூமென்ட்களும் மேலே நகர்த்தப்படுகின்றன. இந்த செயல்பாட்டைப் பயன்படுத்திக்கொண்டு, "self"-ஐ ஒரு ரெஜிஸ்டரில் வெளிப்படையாகச் சேமிக்க வேண்டிய அவசியமின்றி ஸ்டேக்கில் பெறுவதற்கு ஒரு வழி இருந்தது.
இரண்டாவது பகுதியில், முறை பெயரையும் ஸ்டேக்கில் பெறுவது அடங்கும். இதற்காக, நாங்கள் ஒரு தந்திரமான வழியில் SELF ஆப்கோடின் செயல்பாடுகளை மாற்ற வேண்டியிருந்தது.
இயல்புநிலையில், SELF மெம்பர் ஃபங்ஷனைக் கண்டறிந்து, அதை R(A)-இல் இன்ஸ்டன்ஸுடன் சேர்த்து சேமிக்கும் என்பதை நினைவில் கொள்ளுங்கள். நாங்கள் அந்தத் தேடலை முற்றிலுமாகத் தவிர்த்து, உண்மையான ஆப்ஜெக்ட்டை R(A)-இலும், மெத்தட் பெயரை R(A+1)-இலும் சேமித்தோம்.
இப்போது R(A)-இல் உள்ள பொருளில் __call மெட்டாமெத்தட் இருப்பதை நாம் உறுதிசெய்தால், self-ஐயும் ஸ்டேக்கில் தள்ளும் நிலை ஏற்படும். எனவே, [self, method name, args…] போன்ற ஒரு ஸ்டேக் நமக்குக் கிடைக்கும், மேலும் C++-இல் ஒரே ஒரு அழைப்பை மட்டும் செய்யும். அருமை! சரி, ஏறக்குறைய. :)
இதை முடித்துவிட்டதாகக் கருதுவதற்கு முன்பு, நாங்கள் அதில் சில இறுதி மெருகூட்டல்களைச் செய்ய விரும்பினோம். __call மெட்டாமெத்தடின் சொற்பொருளை அதிகமாகச் சுமத்த நாங்கள் விரும்பவில்லை, எனவே அதற்குப் பதிலாக, இந்த வகையான அழைப்புக்காக __namecall என்று அழைக்கப்படும் ஒரு குறிப்பிட்ட மெட்டாமெத்தடைச் சேர்த்தோம், அது UserData ஆப்ஜெக்ட்களில் மட்டுமே கிடைக்கும். ஆப்ஜெக்ட்டில் __namecall மெட்டாமெத்தட் இருந்தால் மட்டுமே புதிய சொற்பொருளைப் பயன்படுத்தும் வகையில் SELF ஆப்கோடையும் நாங்கள் மாற்றியமைத்தோம்.
நாங்கள் செய்த இரண்டாவது விஷயம், புதிய பாதையையும் பழைய பாதையையும் எளிதாக குறியீட்டைப் பகிரும் வகையில் செய்வதாகும். இரண்டாவது வாதமாக முறை-பெயரைக் கொண்டிருப்பதற்குப் பதிலாக, அதை கடைசி வாதமாக மாற்றினோம். எனவே, முறை சுட்டியைக் கண்டறிய அது பயன்படுத்தப்பட்ட பிறகு, அதை எளிதாக ஸ்டேக்கிலிருந்து அகற்ற முடிந்தது, மேலும் பழைய பாதை வழியாகச் செயல்பாடு அழைக்கப்பட்டபோது இருந்ததைப் போலவே ஸ்டேக் தோற்றமளித்தது.
முடிவுரை
இந்த மேம்படுத்தல் எவ்வளவு தாக்கத்தை ஏற்படுத்துகிறது? சரி, நிரலாக்கத்தில் உள்ள பெரும்பாலான விஷயங்களைப் போலவே, இதற்கான பதிலும் "அது சூழ்நிலையைப் பொறுத்தது" என்பதே. அதிக வளங்களைப் பயன்படுத்தும் மற்றும் நீங்கள் அடிக்கடி அழைக்காத செயல்பாடுகளுக்கு, நீங்கள் பெரிய முன்னேற்றத்தைக் காண மாட்டீர்கள். ஆனால், நீங்கள் அடிக்கடி அழைக்கும் சிறிய செயல்பாடுகளுக்கு, சேமிப்பு கணிசமானதாக இருக்கும்.
டெவலப்பர் மன்றத்தில் உள்ளவர்கள் இந்த விசித்திரமான, புதிய மெட்டாமெத்தட் தோன்றியதை விரைவாகக் கவனித்தனர், மேலும் __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 பயன்படுத்தப்படுவதால், நாம் காணும் முடிவுகளில் மகிழ்ச்சியாக இருப்பதால், நமது கவனத்தை நினைவகப் பயன்பாட்டிற்குத் திருப்பி, அந்தப் பகுதியில் கிளையண்டை மேம்படுத்த நாம் என்ன செய்ய முடியும் என்பதைப் பார்க்க வேண்டிய நேரம் இது!


