جاوا اسکریپت چگونه در مرورگر اجرا می شود؟
زمانی که شما به عنوان یک توسعه دهنده به صفحه وب سایت تان جاوا اسکریپت اضافه می کنید، در واقع یک هدف و یک مشکل دارید.
هدف: شما می خواهید به رایانه بگویید که چه کاری انجام دهد.
مشکل: شما و رایانه به زبان های مختلفی صحبت می کنید.
شما به زبان انسان صحبت می کنید و رایانه به زبان ماشین. جاوا اسکریپت یا هر زبان برنامه نویسی سطح بالا، زبان انسانی است؛ حتی اگر شما به آن ها به این دید نگاه نمی کنید. این زبان ها برای تشخیص توسط انسان ها نوشته شده اند نه ماشین ها.
بنابراین وظیفه ی موتور جاوا اسکریپت است که زبان انسانی شما را دریافت کند و آن را به چیزی که ماشین می فهمد تبدیل کند.
من به این موضوع مثل فیلم «ورود» نگاه می کنم، که در آن انسان ها و موجودات فضایی تلاش می کنند با یکدیگر صحبت کنند.
در این فیلم، انسان ها و موجودات فضایی پیام های یکدیگر را فقط به صورت کلمه به کلمه ترجمه نمی کنند. در واقع این دو گروه دو سبک متفاوت برای فکر کردن درباره ی جهان دارند. این موضوع بین انسان ها و ماشین ها نیز صادق است.
پس ترجمه چگونه صورت می پذیرد؟
در برنامه نویسی، به طور معمول دو روش برای ترجمه ی کد به زبان ماشین وجود دارد. شما می توانید از یک مفسر (interpreter) یا کامپایلر (compiler) استفاده کنید. برای توضیحات کامل در مورد مفسر و کامپایلر می توانید مقاله ی «Compiler با Interpreter چه تفاوتهایی دارا است؟» را در سایت سکان آکادمی مطالعه کنید.
مفسر تقریباً کد را خط به خط در حین اجرای برنامه، ترجمه می کند.
در حالی که یک کامپایلر برنامه را در حین اجرا ترجمه نمی کند. کامپایلر قبل از اجرا کل برنامه را ترجمه و آن را در جایی ذخیره می کند.
مزایا و معایب مفسر
با استفاده از مفسرها شما نیاز نیست برای اجرای کدتان کل مرحله کامپایل کردن را طی کنید. به راحتی می توانید اولین خط کد را ترجمه و اجرا کنید. در نتیجه به نظر می آید که استفاده از مفسر برای چیزی مثل جاوا اسکریپت مناسب است. همچنین برای یک توسعه دهنده وب مهم است که کد خود را به راحتی و سرعت اجرا کند. به همین دلیل است که مرورگرها از ابتدا مفسرهای جاوا اسکریپت را استفاده کردند.
اما عیب مفسر، زمانی مشخص می شود که شما یک کد را بیش از یک بار اجرا می کنید. برای مثال، در یک حلقه شما باید یک ترجمه یکسان را بارها و بارها پشت هم انجام دهید.
مزایا و معایب کامپایلر
کامپایلر برعکس مفسر است. با استفاده از کامپایلر زمان بیشتری طول می کشد تا کد ما آماده اجرا باشد. این تأخیر به دلیل مرحله کامپایل شدن کل کد به وجود می آید. اما در ازای این تأخیر کد ما در حلقه ها سریع تر اجرا می شود زیرا نیازی ندارد تا در هر تکرار حلقه، ترجمه آن تکه کد انجام شود.
یک تفاوت دیگر بین کامپایلر و مفسر این است که کامپایلر فرصت دارد تا کل کد را بررسی کند و آن را به صورتی اصلاح و ترجمه کند که برنامه سریع تر اجرا شود. به این کار بهینه سازی می گویند. در نقطه مقابل، مفسر کار خود را در هنگام اجرای برنامه انجام می دهد و نمی تواند زمان زیادی را در فاز ترجمه مصرف کند. بنابراین قابلیت بهینه سازی کد برای اجرا توسط ماشین در مفسر وجود ندارد.
کامپایلرهای درجا: بهترین هر دو جهان
مرورگرها به عنوان راه حلی برای حذف غیر بهینگی مفسرها، شروع به ادغام کردن کامپایلر با مفسر کردند. مرورگرهای مختلف این کار را به روش های نسبتاً متفاوتی انجام می دهند، اما ایده اصلی مشابه یکدیگر است. مرورگرها قسمت جدیدی به موتور جاوا اسکریپت به نام monitor اضافه کردند (این قسمت از موتور جاوا اسکریپت را به اسم profiler هم می شناسند). Monitor همین طور که کد اجرا می شود آن را تحت نظر می گیرد و از تعداد دفعاتی که آن اجرا می شود و چه نوع داده هایی استفاده شده اند، یادداشت برداری می کند.
در ابتدا monitor همه کد را توسط مفسر اجرا می کند.
اگر قسمتی از کد چند مرتبه اجرا شود به آن تکه کد برچسب «گرم» زده می شود. اگر آن قسمت تعداد مرتبه زیادی اجرا شود برچسب «داغ» می گیرد.
کامپایلر پایه (baseline compiler)
هنگامی که یک تابع شروع به گرم شدن می کند (چند مرتبه اجرا می شود)، کامپایلر درجا آن را برای کامپایل شدن ارسال می کند و سپس نتیجه کامپایل را ذخیره می کند.
هر خط از کد به یک stub کامپایل می شود. stub ها بر اساس شماره خط و نوع داده ی متغیر ایندکس می شود (در ادامه اهمیت این موضوع را شرح می دهم). اگر monitor مشاهده کند که تکه کد دوباره با همان نوع داده ها اجرا می شود، از نسخه کامپایل شده استفاده می کند.
این فرایند کمک می کند تا سرعت اجرا بالاتر رود اما یک کامپایلر توانایی های بیشتری دارد. کامپایلر می تواند مقداری وقت صرف کند و بهینه ترین حالت کامپایل کردن کد برای اجرا را پیدا کند.
کامپایلر پایه می تواند مقداری از این بهینه سازی ها را انجام دهد؛ اگرچه که نمی تواند اجرای کد را برای مدت زیادی متوقف کند تا تمام بهینه سازی ها را انجام دهد. اما اگر یک تکه کد خیلی داغ باشد، می توان مدت زمان بیشتری را صرف کنیم و بهینه سازی های بیشتری را اعمال کنیم.
کامپایلر بهینه ساز
هنگامی که یک تکه از کد خیلی داغ است، monitor آن را به کامپایلر بهینه ساز می دهد. با این کار یک نسخه کامپایل شده دیگر از کد را ذخیره می کنیم که از نسخه ی قبلی هم سریع تر است.
به منظور اینکه کامپایلر بهینه ساز یک نسخه سریع تر بسازد، نیاز دارد تا فرض هایی را در نظر بگیرد. برای مثال، اگر کامپایلر بهینه ساز بتواند فرض کند که تمام اشیاء ساخته شده توسط یک constructor یک شکل را دارند (همیشه فیلد های هم اسم دارد و این فیلد ها همیشه با ترتیبی یکسان اضافه شده اند)، می تواند بر اساس این فرض کد را بهینه تر کامپایل کند. کامپایلر بهینه ساز از اطلاعاتی که monitor از تحت نظر گرفتن اجرای کد به دست آورده است استفاده می کند تا این فرض ها را در نظر بگیرد. اگر چیزی در تمام تکرارهای قبلی یک حلقه درست بوده است، کامپایلر بهینه ساز فرض می کند که در باقی تکرارهای حلقه نیز درست خواهد بود.
البته در جاوا اسکریپت (و زبان های dynamic type دیگر) هیچوقت این تضمین وجود ندارد. شما می توانید ۹۹ شیء داشته باشید که شکل یکسانی دارند اما شیء ۱۰۰ ام ممکن است یکی از فیلد ها را نداشته باشد. بنابراین کد کامپایل شده نیاز دارد که صحت فرض های در نظر گرفته شده را قبل از اجرا شدن بررسی کند. اگر فرضیات درست بود، کد کامپایل شده اجرا می شود؛ در غیر این صورت کامپایلر درجا می فهمد که فرضیاتش اشتباه بوده و کد بهینه کامپایل شده را دور می اندازد.
پس از دور انداختن کد بهینه برای اجرا به سراغ کامپایلر پایه یا مفسر می رویم. به این کار deoptimization می گویند.
معمولاً کامپایلرهای بهینه ساز کد را سریعتر می کنند، اما گاهی اوقات می توانند باعث مشکلات عملکردی غیر منتظره ای شوند. اگر کدی دارید که دائماً بهینه سازی و سپس دور انداخته می شود، در نهایت کد شما از حالتی که فقط کد کامپایل شده توسط کامپایلر پایه را اجرا کنید، کند تر خواهد بود.
اکثر مرورگرها محدودیت هایی برای خارج شدن از این چرخه بهینه سازی و دور انداختن نسخه بهینه شده اضافه کرده اند. برای مثال اگر ۱۰ مرتبه این چرخه تکرار شود، کامپایلر درجا دیگر تلاشی برای بهینه کردن کد نمی کند.
یک مثال از بهینه سازی: مشخص کردن نوع داده ها (Type specialization)
انواع مختلفی از بهینه سازی وجود دارد، اما در اینجا بک نوع را به عنوان مثال مطرح می کنیم تا شما بتوانید روند کلی کار را ببینید و به درک مناسبی از اینکه بهینه سازی چگونه رخ می دهد برسید. یکی از بزرگترین آورده های کامپایلر های بهینه ساز، مشخص کردن نوع داده ها است.
سیستم dynamic type ای که جاوا اسکریپت استفاده می کند، زمان بیشتری را در مدت اجرا شدن نیاز دارد. برای مثال کد زیر را در نظر بگیرید.
function arraySum(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
گام += در حلقه ممکن است ساده به نظر برسد و با خود بگویید که این گام در یک مرحله قابل اجرا است. اما به خاطر dynamic type کردن، این گام بیش از آن چیزی که فکر می کنید طول می کشد.
بیایید فرض کنیم که arr آرایه ای از ۱۰۰ عدد صحیح (integer) است. هنگامی که کد گرم می شود، کامپایلر پایه برای هر عملیات در تابع یک stub می سازد. پس برای sum += arr[i]، یک stub ساخته می شود که عملیات += را به عنوان جمع دو عدد صحیح انجام می دهد.
با این وجود ضمانتی وجود ندارد که sum و arr[i] همیشه عدد صحیح باشند. جاوا اسکریپت یک زبان dynamic type است و این احتمال وجود دارد که در تکرارهای بعدی حلقه arr[i] مقداری با نوع رشته (string) داشته باشد. جمع اعداد صحیح و به هم چسباندن دو رشته دو عملیات کاملاً متفاوت اند و به دو صورت کاملاً متفاوت به زبان ماشین کامپایل می شوند.
کامپایلر درجا با کامپایل کردن چندین stub پایه این فرایند را مدیریت می کند. اگر یک تکه کد یک ریخت یا monomorphic (همیشه با نوع داده های مشابهی صدا زده می شود) است، تنها یک stub از آن ساخته می شود. اگر تکه کد چند ریخت یا polymorphic (در هر تکرار نوع داده های تغییر می کند) است، برای هر ترکیبی از جایگشت های انواع داده ها یک stub برای آن ساخته می شود.
این موضوع به این معنا است که کامپایلر درجا باید برای انتخاب یک stub تعداد زیادی سوال بپرسد.
به این دلیل که هر خط از کد مجموعه stub های خودش را دارد، کامپایلر پایه باید در هر بار اجرا شدن آن انواع داده ها را چک کند. پس در هر تکرار از حلقه کامپایلر پایه باید مجموعه سؤالات یکسانی را بپرسد.
اگر کامپایلر درجا نیاز نداشت تا هر بار این سؤالات را بپرسد کد سریع تر اجرا می شد. این کاری است که کامپایلر بهینه ساز انجام می دهد. در کامپایلر بهینه ساز کل تابع با هم کامپایل می شود. بررسی نوع داده ها در این حالت، قبل از حلقه انجام می شود.
بعضی از کامپایلرهای درجا حتی بهینه سازی را بیشتر جلو می برند. برای مثال مرورگر فایرفاکس یک طبقه بندی مخصوص برای آرایه هایی دارد که فقط شامل اعداد صحیح هستند. بنابراین اگر arr از این نوع داده باشد، دیگر نیازی نیست که کامپایلر درجا چک کند که arr[i] یک عدد صحیح است یا خیر. با بهره بردن از این قابلیت کامپایلر درجا می تواند بررسی نوع داده ها را قبل از ورود به حلقه انجام دهد.
جمع بندی
در این مقاله کامپایلر درجا یا JIT Compiler به طور خلاصه توضیح داده شد. این ابزار با بهره بردن از مشاهده کد توسط monitor و ارسال کدهای داغ برای بهینه شدن، باعث می شود جاوا اسکریپت سریع تر اجرا شود. کامپایلر درجا عملکرد اغلب برنامه های جاوا اسکریپتی را بسیار بهبود داده است.
البته با وجود تمام آورده های کامپایلر درجا، عملکرد جاوا اسکریپت می تواند غیر قابل پیش بینی باشد. همچنین برای بهبود عملکرد، کامپایلر درجا هزینه های سرباری را در حین اجرای برنامه به ما اعمال می کند. این هزینه ها شامل موارد زیر می شود.
• بهینه سازی کد و دور انداختن تکه کد
• مصرف حافظه برای تحت نظر گرفتن کد و بازیابی اطلاعات وقتی که تکه کد بهینه شده دور انداخته می شود
• مصرف حافظه برای ذخیره نسخه های کامپایل شده پایه و کامپایل شده بهینه شده تابع
جا برای پیشرفت در این موضوع وجود دارد. هزینه های سربار می توانند کاهش پیدا کنند که می تواند عملکرد آن را قابل پیش بینی تر کند.
امیدوارم این مقاله برای شما مفید بوده باشد. خوشحال می شوم اگر نظر یا سؤالی در مورد مقاله دارید، در قسمت نظرات بنویسید.