جاوااسکریپت در ابتدای شروع کار خود بسیار آهسته بود اما بعدا به لطف چیزی که ما آن را JIT مینامیم بسیار سریعتر شد. اما JIT چگونه کار میکند؟ سوالی که در این مطلب قصد داریم به آن پاسخ دهیم.
جاوااسکریپت چگونه در مرورگر اجرا میشود؟
وقتی که به عنوان یک توسعهدهنده جاوااسکریپت را به برگههای خودتان اضافه میکنید در واقع شما از این کار یک هدف دارید. اما در کنار این موضوع با یک مشکل نیز سر و کار دارید.
مشکل: شما و کامپیوتر با زبانهای متفاوتی صحبت میکنید.
شما با استفاده از یک زبان انسانی ارتباط برقرار میکنید اما این در صورتی است که کامپیوتر تنها چیزی را که متوجه میشود زبان ماشین است. البته در این گذاره ما زبانهای برنامهنویسی سطح بالا را نیز به عنوان زبانهای انسانی در نظر گرفتهایم! این زبانهای برنامهنویسی برای شناخت و درک توسط انسانها طراحی شدهاند، اما برای درک توسط کامپیوتر واقعا مشکل دارند.
بنابراین وظیفهای که به موتور جاوااسکریپت محول شده است، این است که زبان انسانی را به زبانی تبدیل بکند که ماشین و کامپیوتر آن را میشناسد.
من این موضوع را مانند فیلم Arrival میبینم، جایی که انسانها تلاش دارند تا با بیگانگان صحبت بکنند.
در آن فیلم، انسانها و بیگانگان نمیتوانند به صورت کلمه به کلمه موضوعاتی که میخواهند بیان کنند را ترجمه نمایند، به این دلیل که دو گروه، دو شیوه فکری مختلف از دنیا را دارند. این موضوع در ارتباط با رابطه انسان و ماشین نیز به همین شکل است. در این رابطه بیشتر توضیحاتی را ارائه خواهم داد.
حال سوال به این تبدیل شد که ترجمه کدهای جاوااسکریپتی یا زبانهای برنامهنویسی سطح بالا چگونه اتفاق میافتد؟
در دنیای برنامهنویسی به صورت کلی دو راه برای انجام چنین کاری وجود دارد. یا اینکه شما میتوانید از یک مفسر استفاده بکنید و یا اینکه میتوانید سراغ یک کامپایلر بروید.
با استفاده از یک مفسر، عملیات ترجمه به صورت خط به خط در زمان اجرای برنامه اتفاق میافتد.
با این حال یک کامپایلر چنین رویکردی را ندارد. در کامپایلر کدها در حین اجرای برنامه ترجمه نمیشود. بلکه برای ترجمه و تبدیل آن به یک فایل اجرایی مدت زمانی را به خود اختصاص میدهد.
هر کدام از این رویکردها مزایا و معایبی دارند که میتوانید در این قسمت آنها را مطالعه بکنید.
مزایا و معایب مفسر
فرایند کاری مفسرها بسیار سریع است. در این فرایند نیازی نیست که همه چیز را برای اجرا کردن اولیه ترجمه بکنید. شما تنها کافیست که به اولین خط دسترسی داشته باشید تا برنامه اجرا شود و عملیات اجرا شدن شروع شود. بقیه مراحل به صورت خط به خط جلو میرود.
به همین دلیل به نظر میرسد که استفاده کردن جاوااسکریپت از یک مفسر بسیار منطقی باشد. به این دلیل که توسعهدهندگان وب را قادر میسازد تا بتوانند کدهایشان را سریع اجرا نمایند. به همین دلیل است که مرورگرها در ابتدا از مفسر جاوااسکریپت استفاده میکردند.
اما عیبی که برای مفسر وجود دارد زمانی خود را نشان میدهد که شما یک کد را بیشتر از یک بار اجرا بکنید. در چنین حالتی در هر بار اجرای برنامه شما نیاز دارید که فرایند تفسیر و ترجمه را از ابتدا انجام دهید. این کار تا ابد ادامه خواهد داشت.
مزایا و معایب کامپایلر
کامپایلر رویکردی متفاوت را دنبال میکند. برای شروع برنامهای که کامپایل میشود ابتدای کار نیاز است که چند ثانیه منتظر بمانید، این بدان دلیل است که روال کامپایل کردن در ابتدای کار اجرا میشود. اما بعد از آن اگر بخواهید برای بار دوم و… برنامه را اجرا بکنید، همه چیز بسیار سریعتر اجرا میشود و نیازی به ترجمه دوباره برنامهها نیست.
یک مزیت دیگر کامپایلر این است که با دقت بسیار بیشتری به کدها نگاه میکند و در روال بهینهسازی بسیار بیشتر به شما کمک مینماید. به همین دلیل اغلب زبانهای کامپایلری در رفع اشکال بسیار حرفهای تر برخورد میکنند.
اما در نهایت همانطور که گفته شد روال ترجمه کردن معمولا زمان منحصر به فرد خودش را میخواهد و این موضوع روی زمان اجرای برنامه تاثیر گذار خواهد بود.
کامپایلرهای Just in time: بهترینهای هر دو حالت
از آنجایی که میزان بهرهوری مفسرها به دلیل هر بار اجرای کدها بسیار کم است، مرورگرها سعی کردند تا بتوانند قابلیتهای کامپایلری را با حالت جدید ترکیب کنند.
مرورگرهای مختلف چنین کاری را به صورتهای متفاوتی انجام میدهند اما در نهایت ایده اولیه این موضوع ثابت و یکسان است. آنها بخش جدیدی را به موتور جاوااسکریپت اضافه کردند که به آن مانیتور گفته میشود. این مانیتور در زمان اجرای کد به آن نگاه میکند و به تعداد بار اجرای کد و نوعهایی که استفاده میکند، دقت مینماید.
در ابتدا مانیتور تنها کاری که انجام میدهد این است که همه کدها را از طریق یک مفسر اجرا میکند.
در روال اجرا اگر یک خط کد چند بار اجرا شود به آن سگمنت از کد «گرم» میگویند، اما اگر این کد تعداد بسیار بیشتری اجرا شود به آن «داغ» میگویند.
کامپایلر خط مقدم
وقتی که یک تابع شروع به گرم کردن کرد، JIT آن را برای کامپایل شدن ارسال میکند. بعد، این کامپایل در جایی نگهداری و ذخیره میشود.
هر خط از تابع به واحدی تحت عنوان stub کامپایل میشود. Stubها توسط یک سری عدد به صورت خطی ایندکس گذاری میشوند. اگر مانیتور متوجه شود که این تابع با همان نوعهای دادهای خود دوباره اجرا میشود، تنها کاری که برای اجرای آن میکند این است که نسخه کامپایل شده را برای اجرا ارسال میکند.
این موضوع کمک میکند که همه چیز بسیار سریعتر شود. اما همانطور که گفته شد، کارهای بیشتری وجود دارد که یک کامپایلر میتواند انجام دهد. کامپایلر میتواند با نگاه کردن به صورت دقیق به کدها متوجه شود که چه راه مؤثر و بهینه دیگری برای اجرای کدها وجود دارد.
کامپایلر خط مقدم برخی از این بهینهسازیها را انجام میدهد. البته برای اینکار زمان زیادی را نمیخواهد به این دلیل که نمیخواهد روند اجرا را طولانی کند.
با این حال اگر کد واقعا داغ باشد، متوجه خواهیم شد که منتظر ماندن برای انجام یکسری بهینهسازیها میتواند ارزشمند باشد.
کامپایلر بهینهساز
وقتی یک قسمت از کد بسیار داغ میشود، مانیتور آن قطعه از کد را برای اعمال بهینهسازی ارسال میکند. این کار باعث میشود که یک نسخه دیگر و البته سریعتر از تابع ذخیره شود.
برای ایجاد یک نسخه سریعتر از کد، کامپایلر بهینهساز یکسری مفروضات را در نظر میگیرد. برای مثال اگر کامپایلر متوجه شود که تمام شئها ساخته شده مربوط به یک سازنده یک شکل هستند و شباهت دارند، براساس این واقعه، یکسری از کدها را حذف میکند. البته همانطور که از این فرایند معلوم است در خروجی کد تغییری وارد نمیشود بلکه صرفا بهینهسازیهایی اعمال میشود.
کامپایلر برای بدست آوردن چنین مفروضات و قضاوتهایی از اطلاعاتی استفاده میکند که مانیتور در اختیار وی قرار میدهد. اگر تمام قضایا در این روال درست باشد، مفروضات نیز براساس واقعیتهای درستی انجام میشود و این روال را ادامه میدهد.
اما باید در نظر بگیرید که در جاوااسکریپت واقعا هیچگونه شکلی از گارانتی وجود ندارد. ممکن است شما ۹۹ شئ داشته باشید که همه یکسان هستند و خروجی یکسانی دارند اما صدمین شئ در این لیست رفتار متفاوتی را از خود نشان میدهد.
بنابراین در این وضعیت کدهای کامپایل شده نیاز دارند که قبل از اجرا شدن، یک بار برای بررسی کردن درستی فرضیات، اعتبارسنجی شوند. اگر این اعتبارسنجی نتیجه درستی داشته باشد در نهایت کدهای کامپایل شده به اجرا در میآیند. اما در غیر اینصورت JIT متوجه میشود که یک جای کار اشتباه کرده و آنگاه کدهای بهینهسازی شده را حذف میکند.
بعد از حذف این نسخه، روند اجرا به نسخه تفسیر شده و یا نسخهای که کامپایلر خط مقدم تهیه کرده است برمیگردد. این روند را deoptimization میگویند.
به صورت کلی کامپایلرهای بهینهساز باعث میشوند که کد سریعتر اجرا شود اما همین موارد برخی اوقات باعث میشوند که خطاهای کارایی غیر قابل انتظاری بوجود بیاید. اگر شما کدهایی داشته باشید که بهینهسازی شده اما در نهایت دور انداخته شدهاند، با نسخهای کندتر برای اجرای کد باید اقدام نمایید.
مرورگرها باید محدودیتهایی را برای سیکل بهینهسازی و deoptimization در نظر بگیرند.
یک مثال از بهینهسازی: Type Specialization
نوعهای متفاوتی از بهینهسازی وجود دارد اما من میخواهم شما را با یک نوع از این بهینهسازی آشنا کنم تا روال بهینهسازی را بهتر درک بکنید. یکی از بزرگترین کارهایی که کامپایلرهای بهینهساز انجام میدهند چیزی به نام Type Specialization است.
زبانهای برنامهنویسی که از سیستم نوع دادهای پویا یا داینامیک استفاده میکنند، مدت زمان بیشتری را برای اجرا میطلبند. برای مثال کد زیر را در نظر بگیرید:
function arraySum(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
به نظر میرسد که قسمت += ساده باشد. شاید فکر کنید که محاسبه کردن این مورد تنها در یک قدم انجام میشود اما به دلیل آنکه اینجا از نوع دادهای پویا استفاده میشود، روال اجرا شدن کمی پیچیده است.
بیایید تصور کنیم که arr یک آرایه حاوی ۱۰۰ خانه عدد صحیح یا int است. زمانیکه کد اجرا شود، کامپایلر خط مقدم یک stub را برای هر عملیات در تابع ایجاد میکند. بنابراین ما یک stub را برای sum += arr[i] خواهیم داشت که در آن عملیات += مدیریت میشود.
با این حال sum و arr[i] به صورت تضمینی معلوم نیست که عدد صحیح باشند. به این دلیل که نوعهای دادهای در جاوااسکریپت به صورت پویا تعریف میشوند، پس شانسی وجود دارد که در حلقه تکرارشدنی arr[i] بجای یک عدد صحیح یک رشته باشد. قرار دادن دو رشته در کنار همدیگر یا concatenation با قرار دادن دو عدد و جمع دادن آنها با همدیگر addition عملیاتهای کاملا متفاوتی هستند و کد ماشین مربوط به آنها نیز کاملا متفاوت است.
راهی که JIT چنین موضوعی را مدیریت میکند، از طریق کامپایل کردن stubهای خط مقدم مختلف است. اگر یک قسمت از کد همواره یک نوع دادهای داشته باشد تنها یک stub دریافت میکند. اما اگر نوعهای دادهای متفاوتی وجود داشته باشد برای هر ترکیبی از نوعهای دادهای stubهای متفاوتی وجود خواهد داشت.
این بدان معناست که JIT برای انتخاب stub درست باید سوالات بسیار زیادی را بپرسد و این موضوع زمان بر است.
به این دلیل که هر خط از کد، مجموعهای از stubهای مربوط به خودش را در کامپایلر خط مقدم دارد، JIT باید بتواند نوعهای دادهای را در هر بار اجرا بررسی کند. بنابراین برای هر تکرار از حلقه نیاز است که روال بررسی را انجام دهد و سوال تکراری را بپرسد.
اگر JIT مجبور نباشد که این سوالها را هر بار بپرسد در نتیجه کد بسیار سریعتری خواهیم داشت. این دقیقا کاری است که کامپایلر بهینهساز انجام میدهد.
در کامپایلر بهینهساز تمام تابع با همدیگر کامپایل میشود. بررسیهای مربوط به نوع دادهای در این شرایط، قبل از اجرای حلقه انجام شده و دیگر بررسی خاتمه پیدا میکند.
برخی از بهینهسازیهای JIT حتی عمیقتر نیز وارد مسئله میشوند. برای مثال در فایرفاکس یک طبقهبندی منحصر به فرد برای آرایهها وجود دارد که تنها شامل اعداد صحیح است. اگر arr را یکی از این آرایهها بدانیم بنابراین JIT نیازی ندارد arr[i] را برای هر بار بررسی عدد صحیح بودن بررسی بکند. این بدان معناست که JIT میتواند تمام این بررسیهای نوعی را قبل از وارد شدن به حلقه انجام دهد.
در پایان
این هم از بحث JIT به صورت بسیار خلاصه شده! JIT باعث میشود که جاوااسکریپت بسیار سریعتر شود. برای اینکار همانطور که مشاهده کردید از روشهای مختلفی استفاده میکند که هر کدام از این موارد با تکنیکی دیگر بهینهتر و بهتر شد. حتی با تمام این بهبودها باز هم کارایی جاوااسکریپت میتواند غیرقابل پیشبینی باشد.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید