లువా/సి++ పరస్పర చర్యను ఆప్టిమైజ్ చేయడం

పరిచయం
రాబ్లాక్స్ ఇంజిన్ C++ మరియు Lua కలయికలో వ్రాయబడింది, అధిక గణన శక్తితో కూడిన కార్యకలాపాలను నిర్వహించే కోడ్ ఆప్టిమైజ్ చేయబడిన C++లో వ్రాయబడింది, అయితే డెవలప్మెంట్ సులభతరం కోసం గేమ్ లాజిక్ మరియు స్క్రిప్ట్లు Luaలో వ్రాయబడ్డాయి. ఈ మోడల్ సమర్థవంతంగా పనిచేయాలంటే, Lua మరియు C++ మధ్య మార్పులు వీలైనంత వేగంగా జరగాలి, ఎందుకంటే ఈ మధ్యస్థ దశలో గడిపే ఏ సమయమైనా ప్రాథమికంగా వృధా అయిన మిల్లీసెకన్లు మాత్రమే.
గత కొన్ని నెలలుగా, మేము సిస్టమ్ యొక్క ఈ భాగానికి వివిధ మెరుగుదలలను అందిస్తున్నాము. ప్రత్యేకంగా ఒక భాగం—అంటే లూయా నుండి C++ మెథడ్లను వాస్తవంగా పిలవడం—అత్యంత ఆసక్తికరంగా ఉంది, ఎందుకంటే ఇది గణనీయమైన వేగ మెరుగుదలలకు దారితీసింది మరియు అంతర్గతంగా పనులు ఎలా జరుగుతాయో అర్థం చేసుకోవడానికి లూయా యొక్క మూలాలను పరిశీలించాల్సి వచ్చింది.
మేము చివరికి Lua VMనే సవరించాము, కానీ దానికి వెళ్ళే ముందు, మనం కొంత ప్రాథమిక సమాచారం తెలుసుకోవాలి.
కంపైలర్లు, VM, మరియు బైట్కోడ్
లూవా సోర్స్ కోడ్ కంపైల్ చేయబడినప్పుడు, అది లూవా బైట్కోడ్గా కంపైల్ చేయబడుతుంది, దానిని లూవా VM అప్పుడు రన్ చేస్తుంది. లూవా బైట్కోడ్లో మొత్తం మీద సుమారు 35 ఆదేశాలు ఉంటాయి, టేబుల్స్ను చదవడం/రాయడం, ఫంక్షన్లను కాల్ చేయడం, బైనరీ ఆపరేషన్లను నిర్వహించడం, జంప్లు మరియు కండిషనల్స్ వంటి పనుల కోసం. చాలా ఇతర VMల వలె స్టాక్-ఆధారితంగా కాకుండా, లూయా VM రిజిస్టర్-ఆధారితమైనది, కాబట్టి బైట్కోడ్ను రూపొందించేటప్పుడు కంపైలర్ చేసే పనులలో ఒక భాగం, ప్రతి ఇన్స్ట్రక్షన్ ఏ రిజిస్టర్లను ఉపయోగించాలో నిర్ణయించడం.
ప్రతి సూచన "OP_CODE A B," లేదా "OP_CODE A B C" రూపంలో ఉంటుంది, ఇక్కడ "OP_CODE" అనేది ఆప్కోడ్ (ఉదాహరణకు, ఒక ఫంక్షన్ను కాల్ చేయడానికి CALL), మరియు A/B/C అనేవి ఆప్కోడ్ ఆర్గ్యుమెంట్లు. ఆర్గ్యుమెంట్లు (లేదా రిజిస్టర్లు) వాస్తవ విలువలు కావు. బదులుగా, అవి రెండు టేబుల్స్లో ఒకదానిలోకి సూచించే సూచికలు: కాన్స్టంట్ టేబుల్ (Kst(..)) లేదా రిజిస్టర్ టేబుల్ (R(..)).
లూయా బైట్కోడ్ యొక్క వివరణాత్మక వివరణ కోసం, "ఎ నో-ఫ్రిల్స్ ఇంట్రడక్షన్ టు లూయా 5.1 VM ఇన్స్ట్రక్షన్స్" చూడండి. ఇది వినడానికి ఉన్నదానికంటే చాలా ఎక్కువ ఉత్తేజకరమైనది; నేను హామీ ఇస్తున్నాను!
లూయా బైట్కోడ్ ఎలా ఉంటుందో మీకు ఒక అవగాహన రావడానికి, మనం మొదట కొన్ని సాధారణ ప్రోగ్రామ్లను చూసి, ఆ తర్వాత మరింత సంబంధిత ఉదాహరణలకు వెళ్తాము.
చంక్స్పై (Chunkspy) యుటిలిటీని ఉపయోగించి, మనం లూవా బైట్కోడ్ను లూవా అసెంబ్లీగా డిస్అసెంబుల్ చేసి, కోడ్తో పాటు కాన్స్టంట్ టేబుల్ యొక్క లిస్టింగ్ను కూడా పొందవచ్చు, తద్వారా ఇచ్చిన ఏదైనా లూవా సోర్స్ కోడ్ కోసం ఏ బైట్కోడ్ జనరేట్ అవుతుందో మనం వాస్తవంగా చూడగలం.
ప్రాథమిక బైట్కోడ్ ఉదాహరణలు
"x = 10" వంటి ఒక సాధారణ ప్రోగ్రామ్ కంపైల్ అయినప్పుడు:
.const "x"; 0
.const 10; 1
[1] loadk 0 1 ; 10
[2] setglobal 0 0 ; x మొదటి రెండు లైన్లు స్థిర పట్టికను చూపుతాయి (స్లాట్ 0లో "x" స్ట్రింగ్ విలువ మరియు స్లాట్ 1లో 10 పూర్ణాంక విలువతో), మరియు తర్వాతి రెండు లైన్లు డిస్అసెంబుల్ చేయబడిన ఆపకోడ్లు.
[లైన్ 1] "నో ఫ్రిల్స్"లో 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 జోడించబడాలి). ఫంక్షన్ను కాల్ చేసే ముందు, R(A)లోని విలువ వాస్తవానికి కాల్ చేయగలదో లేదో VM కూడా ధృవీకరిస్తుంది.
ఇప్పటికే ఉన్న టేబుల్కు మెట్టేబుల్ను అనుబంధించడం ద్వారా టేబుల్స్ యొక్క కార్యాచరణను విస్తరించడానికి లూయాలో ఒక మెకానిజం ఉంది. ప్రధాన టేబుల్పై ఒక నిర్దిష్ట పద్ధతి లేదా ఆపరేషన్ చేయలేనప్పుడు, మెట్టేబుల్ ఫాల్బ్యాక్ పద్ధతులను ప్రారంభిస్తుంది (పూర్తి వివరణ కోసం https://www.lua.org/pil/13.html చూడండి).
మన అవసరాల కోసం, మెటాటేబుల్లోని అత్యంత సంబంధిత ఎంట్రీలు "__index" మరియు "__call" ఫీల్డ్లు. ఒక టేబుల్లో ఒక ఎలిమెంట్ను వెతికేటప్పుడు __index ఉపయోగించబడుతుంది, కాబట్టి "local x = my_table[10]" కోడ్ మొదట my_table పై __index మెథడ్ను కాల్ చేస్తుంది. అది విఫలమైతే, దానికి బదులుగా my_table యొక్క మెటాటేబుల్పై __index ను కాల్ చేయడానికి ప్రయత్నిస్తుంది. ఉదాహరణకు, మీరు ఒకదాన్ని ఫంక్షన్గా పరిగణించి "local x = foo(42)," అని కాల్ చేయడానికి ప్రయత్నించినప్పుడు, అదే విధంగా __call ఉపయోగించబడుతుంది.
లూయా మరియు C++ పరస్పరం పనిచేయడానికి, అవి ఫంక్షన్లు మరియు డేటాను పంచుకోవడానికి ఒక మార్గం అవసరం. లూయా యూజర్డేటా అనే డేటా రకాన్ని అందించడం ద్వారా దీనికి వీలు కల్పిస్తుంది. C++ లో యూజర్డేటా ఆబ్జెక్ట్లను సృష్టించవచ్చు, మరియు అవి స్థానిక లూయా డేటా రకాలు కాబట్టి, వాటికి మెటాటేబుల్స్ను జోడించవచ్చు. దీనివల్ల, లూయా కోడ్ వాటిని సాధారణ లూయా ఆబ్జెక్ట్లుగా భావించి వాటితో పరస్పరం చర్య జరపగలదు.
సభ్య ఫంక్షన్ పిలుపులు
సరే, ఇప్పుడు కొంత బైట్కోడ్ను చూద్దాం! ఈ తదుపరి ఉదాహరణ కొంచెం ఆసక్తికరంగా ఉంది, ఎందుకంటే ఇది "foo:bar(10)," వంటి కోడ్ ఉన్నప్పుడు ఏమి జరుగుతుందో చూపిస్తుంది, ఇది ఫూ ఇన్స్టాన్స్పై (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 ఇక్కడ
కొత్త విషయం సెల్ఫ్ ఇన్స్ట్రక్షన్ [లైన్ 2], దీనిని మనం ఇంతకు ముందు చూడలేదు. సెల్ఫ్ యొక్క సింటాక్స్ "SELF A B C --- R(A) := R(B)[RK(C)]; R(A+1) := R(B)," కాబట్టి దీనిని విడమరిచి చూద్దాం. రిజిస్టర్ టేబుల్లో R(A) స్లాట్లో, RK(C) స్లాట్లోని కీని ఉపయోగించి టేబుల్లో వెతకడం ద్వారా వచ్చిన ఫలితాన్ని R(B) రిజిస్టర్ స్లాట్లో ఉంచుతుంది. అది R(B) స్లాట్లో ఉన్నదాన్ని R(A+1) స్లాట్లోకి కూడా కాపీ చేస్తుంది, కానీ దీని గురించి తర్వాత మరిన్ని వివరాలు. C రిజిస్టర్ విలువ 257 అని మీరు గమనించవచ్చు. ఇది సరైనదే ఎందుకంటే లూయా విలువను వెతకడానికి RK(C)ని ఉపయోగిస్తోంది, మరియు 9వ బిట్ విలువను బట్టి RK రిజిస్టర్ టేబుల్ లేదా కాన్స్టంట్ టేబుల్ను ఉపయోగిస్తుంది. అది 1 అయితే, ఈ సందర్భంలో అలాగే ఉంది, అప్పుడు కాన్స్టంట్ టేబుల్ ఉపయోగించబడుతుంది; లేకపోతే, (అత్యధిక బిట్ను మాస్కింగ్ చేసిన తర్వాత) లుకప్ రిజిస్టర్ టేబుల్కు వెళుతుంది.
3వ లైన్ స్లాట్ 2లో 10ని ఉంచుతుంది, మరియు చివరగా 4వ లైన్ ఫంక్షన్ కాల్ను ఎగ్జిక్యూట్ చేస్తుంది.
SELF ఇన్స్ట్రక్షన్ రెండు ప్రయోజనాలను నెరవేరుస్తుంది. మొదట, ఇది Foo క్లాస్లో "bar" మెథడ్ కోసం వెతుకుతుంది మరియు దానిని R(A) లో ఉంచుతుంది. రెండవది, foo ఒక ఇన్స్టాన్స్ మెథడ్ కాబట్టి మరియు కాల్ చేసేటప్పుడు మనం ఏ క్లాస్పై మెథడ్ను ఇన్వాయిక్ చేస్తున్నామో ఆ క్లాస్ యొక్క ఇన్స్టాన్స్ మనకు అవసరం కాబట్టి, అది ఈ ఇన్స్టాన్స్ను R(A+1) లో ఉంచుతుంది. మీకు పైథాన్లోని క్లాస్లతో పరిచయం ఉంటే, మీరు ఈ భావనను గుర్తించవచ్చు: మెథడ్లు సాధారణంగా "def my_method(self, arg1, arg2..)" అని వ్రాయబడతాయి, ఇక్కడ self అనేది క్లాస్ ఇన్స్టాన్స్.
మనం దీనిని కొంచెం లోతుగా పరిశీలించాల్సి ఉంటుంది మరియు foo ఇన్స్టాన్స్ అనేది C++ ఆబ్జెక్ట్ అయి, Luaలో UserData ఆబ్జెక్ట్గా ప్రాతినిధ్యం వహించినప్పుడు ఏమి జరుగుతుందో చూద్దాం.
SELF కాల్ను ఒక టేబుల్ లుకప్గా చూడవచ్చు, అంటే Foo["bar"] (పెద్ద అక్షరాలతో ఉన్న Foo అనేది ఇన్స్టాన్స్ fooకు బదులుగా, Foo అనే క్లాస్ను సూచిస్తుంది), మరియు లుకప్లు __index మెథడ్ను ఉపయోగిస్తాయని మనకు తెలుసు. C++ లో foo ఇన్స్టాన్స్ సృష్టించబడినప్పుడు, ఆ ఇన్స్టాన్స్తో ఒక మెటాటేబుల్ అనుబంధించబడింది, మరియు ఆ మెటాటేబుల్లో దాని __index ఫీల్డ్, __index ఇన్వాకేట్ చేయబడినప్పుడు కాల్ చేయబడే C++ కోడ్కు సెట్ చేయబడింది.
లూయా నుండి C/C++ పిలువబడినప్పుడు, పంపబడే ఏకైక డేటా lua_State ఆబ్జెక్ట్. ఈ ఆబ్జెక్ట్లో ప్రస్తుతం నడుస్తున్న లూయా థ్రెడ్కు సంబంధించిన ప్రతిదీ ఉంటుంది. స్టేట్ ఆబ్జెక్ట్లోని అత్యంత ముఖ్యమైన సమాచారం లూయా స్టాక్, ఇందులో ఫంక్షన్ ఆర్గ్యుమెంట్లు (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;
}
}
చాలా అంతర్గత వివరాలు సరళీకరించబడ్డాయి, కానీ దాని సారాంశం ఇది. లూయా స్టాక్లో మొదటి ఆర్గ్యుమెంట్గా పంపబడిన యూజర్డేటా ఆబ్జెక్ట్ను బట్టి, వాస్తవ C++ క్లాస్ను వివరించే ఒక డిస్క్రిప్టర్ను మేము కనుగొనగలుగుతున్నాము, మరియు ఆ డిస్క్రిప్టర్ ద్వారా ఈ క్లాస్కు ఇచ్చిన పేరుతో ఒక మెథడ్ ఉందో లేదో చూడగలం. ఒకవేళ ఉంటే, ఒక మెథడ్ ఇన్వాకర్కు ఫంక్షన్ పాయింటర్ లూయా స్టాక్పైకి పంపబడుతుంది, మరియు మేము విజయాన్ని (success) తిరిగి ఇస్తాము.
ఈ కాల్ తర్వాత, లూయా 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ని ఉపయోగిస్తుంది, కానీ ఈసారి ఇది మెంబర్ ఫంక్షన్ను ఇన్వాక్ చేయగలదు మరియు స్టాక్ నుండి సరైన ఆర్గ్యుమెంట్లను పాప్ చేయగలదు.
చివరి దశ!
ఇప్పుడు లూయా నుండి C++ వరకు జరిగే రెండు రౌండ్ ట్రిప్లను మనం స్పష్టంగా చూడగలుగుతున్నాము కాబట్టి, దీనిని ఎలా ఆప్టిమైజ్ చేయాలో తెలుసుకోవడానికి ప్రయత్నిద్దాం.
మా అంతిమ లక్ష్యం Lua నుండి C++కు ఒకే ఫంక్షన్ కాల్ చేయడం మరియు ఒకేసారి మెథడ్ లుక్అప్ మరియు ఇన్వాకేషన్ చేయడానికి అవసరమైన అన్ని భాగాలను Lua స్టాక్లో ఉంచుకోవడం. సమస్య ఏమిటంటే మనకు ఒక రిజిస్టర్ తక్కువగా ఉంది. మనం మన కలయిక లుకప్/ఇన్వోకర్ ఫంక్షన్ను పిలిచినప్పుడు, లూయా స్టాక్ [self, method name, arg1, arg2, ...] లాగా ఉండాలని మనం కోరుకుంటాము, కానీ SELFని చూస్తే, అది మెథడ్ ఫంక్షన్ను వెతకడం ద్వారా వచ్చిన ఫలితాన్ని తన మొదటి స్లాట్లో మరియు ఇన్స్టాన్స్ను నిల్వ చేయడానికి రెండవ స్లాట్ను ఉపయోగిస్తుందని మనం చూస్తాము.
__call మెటామెథడ్ పనిచేసే విధానాన్ని పరిశీలించినప్పుడు మాకు ఒక కీలకమైన విషయం అర్థమైంది. ఒక ఆబ్జెక్ట్కు __call మెటామెథడ్ ఉంటే, _call ఫంక్షన్ ప్రారంభించబడటానికి ముందు, ఆ ఆబ్జెక్ట్ స్వయంగా స్టాక్పైకి నెట్టబడుతుంది మరియు అన్ని ఆర్గ్యుమెంట్లు పైకి జరపబడతాయి. ఈ కార్యాచరణను ఉపయోగించుకోవడం ద్వారా, ఒక రిజిస్టర్లో స్పష్టంగా "self"ను నిల్వ చేయవలసిన అవసరం లేకుండానే దానిని స్టాక్పైకి తీసుకురావడానికి ఒక మార్గం ఉంది.
రెండవ భాగంలో మెథడ్ పేరును కూడా స్టాక్లోకి తీసుకురావడం ఉండేది. దీనికోసం, మేము కొద్దిగా తెలివిగా వ్యవహరించి SELF ఆప్కోడ్ యొక్క పనితీరును మార్చాము.
డిఫాల్ట్ కేసులో, SELF మెంబర్ ఫంక్షన్ను కనుగొని, దానిని R(A+1)లోని ఇన్స్టాన్స్తో పాటు 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 అమలు చేయబడినందున, మరియు మనం చూస్తున్న ఫలితాలతో సంతృప్తిగా ఉన్నందున, మన దృష్టిని మెమరీ వినియోగం వైపు మళ్లించి, ఆ రంగంలో క్లయింట్ను మెరుగుపరచడానికి మనం ఏమి చేయగలమో చూసే సమయం ఆసన్నమైంది!


