লুয়া/সি++ আন্তঃকার্যক্ষমতা অনুকূলীকরণ

ভূমিকা
Roblox ইঞ্জিনটি C++ এবং Lua-এর সংমিশ্রণে লেখা হয়েছে, যেখানে গণনামূলকভাবে ভারী অপারেশনগুলো অপ্টিমাইজড C++-এ লেখা হয়েছে, আর গেম লজিক ও স্ক্রিপ্টগুলো ডেভেলপমেন্টের সুবিধার্থে Lua-তে লেখা হয়েছে। এই মডেল কার্যকর হতে Lua এবং C++-এর মধ্যে ট্রানজিশন যত দ্রুত সম্ভব হতে হবে, কারণ এই 'নো ম্যান্স ল্যান্ড'-এ ব্যয় করা প্রতিটি মিলিসেকেন্ডই মূলত অপচয়।
গত কয়েক মাসে, আমরা সিস্টেমের এই অংশে বিভিন্ন উন্নতি প্রয়োগ করেছি। বিশেষ করে একটি অংশ—lua থেকে C++ মেথড আহ্বান—বিশেষভাবে আকর্ষণীয় ছিল, কারণ এটি উল্লেখযোগ্য গতি বৃদ্ধি এনেছিল এবং এর জন্য Lua-এর অন্তর্নিহিত কাজগুলো বুঝতে এর মূল অংশে অনুসন্ধান করতে হয়েছিল।
আমরা শেষ পর্যন্ত Lua VM-ই পরিবর্তন করেছি, তবে তার আগে কিছু ভিত্তি স্থাপন করতে হবে।
কম্পাইলার, VM এবং বাইটকোড
যখন Lua সোর্স কোড কম্পাইল করা হয়, তখন এটি Lua বাইটকোডে কম্পাইল হয়, যা Lua VM পরে চালায়। Lua বাইটকোডে মোট প্রায় ৩৫টি নির্দেশনা রয়েছে, যেমন টেবিল পড়া/লেখা, ফাংশন কল করা, বাইনারি অপারেশন সম্পাদন, জাম্প এবং শর্তন ইত্যাদি। অনেক অন্যান্য VM-এর মতো স্ট্যাক-ভিত্তিক না হয়ে Lua 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 প্রথম দুইটি লাইন কনস্ট্যান্ট টেবিল দেখায় (শ্লট 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)-তে থাকা মানটি প্রকৃতপক্ষে কলযোগ্য।
Lua-তে এমন একটি ব্যবস্থা আছে যা ব্যবহারকারীদের বিদ্যমান টেবিলের সাথে একটি মেটাটেবিল যুক্ত করে টেবিলের কার্যকারিতা বাড়ানোর সুযোগ দেয়। মেটাটেবিলে ফ্যালব্যাক মেথড থাকে, যেগুলো প্রধান টেবিলে কোনো নির্দিষ্ট মেথড বা অপারেশন সম্পাদন করা না গেলে আহ্বান করা হয় (পুঙ্খানুপুঙ্খ বর্ণনার জন্য দেখুন 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(A+1) স্লটে অনুলিপি করবে, তবে এ বিষয়ে পরে আরও আলোচনা করা হবে। আপনি লক্ষ্য করতে পারেন যে C রেজিস্টারের মান 257। এটি বৈধ কারণ Lua মান অনুসন্ধানের জন্য RK(C) ব্যবহার করছে, এবং RK নবম বিটের মান অনুযায়ী রেজিস্টার টেবিল বা কনস্ট্যান্ট টেবিল ব্যবহার করবে। যদি এটি 1 হয়, যা এই ক্ষেত্রেই হয়েছে, তাহলে কনস্ট্যান্ট টেবিল ব্যবহার করা হবে; অন্যথায়, সর্বোচ্চ বিট মাস্ক করার পর লুকআপ রেজিস্টার টেবিলে চলে যাবে।
লাইন ৩ স্লট ২-এ ১০ স্থাপন করে, এবং অবশেষে লাইন ৪ ফাংশন কলটি কার্যকর করবে।
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 মেথড ব্যবহার করে। যখন 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 বাকি আর্গুমেন্টগুলো রেজিস্টার টেবিলে রাখবে, এবং তারপর metaIndex মেথড থেকে আমরা যে ফাংশনটি রিটার্ন করেছি সেটি কল করবে, যা আবার 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 স্ট্যাকে সমস্ত প্রয়োজনীয় উপাদান রাখা যাতে একবারে মেথড লুকআপ এবং ইনভোকেশন করা যায়। সমস্যাটি মনে হচ্ছে আমাদের কাছে একটি রেজিস্টার কম। যখন আমরা আমাদের সম্মিলিত লুকআপ/ইনভকার ফাংশন কল করি, তখন আমরা চাই লুয়া স্ট্যাকটি [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, মেথড নাম, আর্গস…] এর মতো দেখাবে এবং এটি 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 প্রয়োগ করা হয়েছে, এবং আমরা যে ফলাফল দেখছি তাতে আমরা সন্তুষ্ট, তাই আমাদের মেমরি ব্যবহারের দিকে মনোযোগ দেওয়ার সময় এসেছে, এবং দেখার যে আমরা সেই ক্ষেত্রে ক্লায়েন্টকে উন্নত করতে কী করতে পারি!


