وب اسمبلی (WebAssembly) – به زبان ساده


در این مقاله به بررسی وب اسمبلی خواهیم پرداخت. مفهوم آن را معرفی میکنیم و میگوییم که قرار است چه مشکلاتی را حل کند. وباسمبلی که به اختصار WASM نامیده میشود، یک قالب دستورهای باینری است که برای ماشین مجازی مبتنی بر پشته استفاده میشود. وباسمبلی به عنوان یک هدف پرتابل برای کامپایل زبانهایی مانند C/C++/Rust طراحی شده است و به این ترتیب امکان توسعه اپلیکیشنهای کلاینت و سرور را روی وب ممکن میکند.
ما در این راهنما برای این که درک بهتری از این فناوری داشته باشیم، یک الگوریتم نوشتیم که امکان مقایسه عملکرد WASM را در برابر جاوا اسکریپت محض (Vanilla JS) فراهم میسازد.
بازی زندگی (Life Game)
برای اثبات مفاهیم ارائه شده از بازی زندگی طراحی شده از سوی Conway به عنوان مسئله استفاده کردهایم. این بازی بدون بازیکن است و قواعد سادهای دارد:
- دنیا از یک ماتریس تشکیل یافته است که در آن هر سلول دو حالت زنده یا مرده دارد.
- تنها ورودی بیرونی، حالت اولیه است.
- تعامل سلول جاری با سلول همجوار افقی، عمودی و قطری حالت کنونی سلول را تعیین میکند.
- یک سلول زنده که کمتر از دو همسایه آن زنده باشند، میمیرد.
- یک سلول زنده که دو یا سه همسایهاش زنده باشند، برای نسل بعدی زنده میماند.
- یک سلول زنده که بیش از سه سلول همسایهاش زنده باشند، میمیرد.
- یک سلول مرده که دقیقاً سه سلول همسایهاش زنده باشند، زنده میشود.
بنابراین طرح ما این است که یک ماتریس بزرگ داشته باشیم و آن را با مقادیر تصادفی (0 یا 1) پر کنیم و این حالت اولیه را ارسال کرده و نتیجه را رندر کنیم، سپس حالت بعدی را محاسبه کرده و آن را مجدداً رندر کنیم و این مرحله اخیر را چندین بار تکرار کنیم.
ما میخواهیم این راهحل را با سه راهبرد پیادهسازی کنیم: جاوا اسکریپت خالص، وباسمبلی، و وب ورکرها. پیچیدگی زمانی الگوریتم ما روی همه رویکردها برابر با (O(m*m است که n عرض دنیا و m ارتفاع آن است. از آنجا که رندر برای هر سه رویکرد یکسان است، آن را در اندازهگیریهای خود لحاظ نمیکنیم.
جاوا اسکریپت محض
معماری زیرساختی برای این رویکرد شامل ایجاد یک بازی جدید و سپس ایجاد و ارسال حالت نخست (ماتریسی پر شده از 0 و 1) به آن است. کامپوننت game این حالت را نگهداری میکند و تابعی به نام next بازمیگرداند که حالت بعدی را هنگام فراخوانی بازگشت میدهد. در این صورت تابع ()getNextState را از فایل environment.js فراخوانی میکنیم که پیادهسازی جاوا اسکریپت خالص است.
داخل کامپوننت environment.js همچنان مسئله را به تابعهای تخصصی کوچکتر افراز میکنیم. بدین ترتیب به روشی آسانتر میتوانیم بهینهسازی کامپایلر JIT را تحریک کنیم. این بهینهسازیها را در مقاله بعدی بررسی خواهیم کرد. این تابعها به محاسبه حالت کنونی همسایههای فوقانی، تحتانی و کناری پرداخته و همه حالتهای گوشهای را پوشش میدهند.

ممکن است از این که چقدر این محاسبات برای حالت بعدی متفاوت هستند و یا این که چرا این همه تابع وجود دارد شگفتزده شوید. برای پاسخ به این سؤال باید با طرز کار کامپایلرهای JIT آشنا باشیم و بدانیم که چگونه این وضعیت موجب شده است که جاوا اسکریپت امروزه تا این حد سریع باشد. در بخش بعدی این نوشته این موضوع را بررسی میکنیم.
اندکی از تاریخچه جاوا اسکریپت
جاوا اسکریپت در سال 1995 از سوی «برندن آیک» (Brendan Eich) طراحی شد و هدف وی ارائه زبانی بود که طراحان به کمک آن بتوانند اینترفیسهای دینامیک را با آن به سادگی پیادهسازی کنند. به بیان دیگر جاوا اسکریپت برای این ساخته نشده که سریع باشد؛ بلکه هدف اولیه این بود که لایه رفتاری را به صفحههای HTML به روشی راحت و سرراست اضافه کند.

جاوا اسکریپت در ابتدا یک زبان تفسیری بود. بدین ترتیب فاز آغازین سریعتر میشد، چون مفسر تنها کافی بود که خط نخست کد را بخواند تا بتواند آن را به بایتکد ترجمه کرده و به طرز صحیح اجرا کند. برای نیازهای اینترنت در دهه 1990 میلادی، جاوا اسکریپت این کار را به طرز خوبی انجام میداد. مشکل زمانی بروز کرد که اپلیکیشنها رفتهرفته پیچیدهتر شدند.
در دهه 2000 میلادی فناوریهایی مانند Ajax موجب شدند که وب اپلیکیشنها، پویاتر شوند، جیمیل در سال 2004 و گوگل مپ در سال 2005 آغازگر روندی برای استفاده از این فناوری ایجکس بودند. این روش جدید برای ساخت وب اپلیکیشنها موجب شد که بیشتر بخش منطقی برنامه در سمت کلاینت نوشت شود. در این زمان جاوا اسکریپت باید عملکرد خود را ارتقا میداد و این اتفاق در سال 2008 با ظهور گوگل و موتور V8 آن که همه کدهای جاوا اسکریپت را به طور بیدرنگ به بایتکد کامپایل میکرد رخ داد. اما اینک شاید بپرسید طرز کار کامپایلرهای JIT چگونه است؟
آشنایی با طرز کار کامپایلرهای JIT
اگر بخواهیم کامپایلرهای JIT را به طور خلاصه توضیح دهیم، زمانی که کد بارگذاری شد، کد منبع به یک بازنمایی درختی تبدیل میشود که «درخت ساختار مجرد» (Abstract Syntax Tree) یا AST نامیده میشود. پس از آن بسته به این که از چه موتور/سیستم عامل/پلتفرمی استفاده میشود، یا یک نسخه مبنا از کد کامپایل میشود و یا بایتکد تولید میشود که باید تفسیر شود.
در این مرحله profiler به رصد و گردآوری دادههای اجرای کد میپردازد. البته این توضیح بسیار مختصر بوده و تفاوتهایی در میان موتورهای مرورگر مختلف در این زمینه وجود دارد.
در گام نخست، همه چیز از تفسیر عبور میکند، این فرایند تضمین میکند که کد پس از ایجاد AST سریعتر اجرا میشود. زمانی که قطعه کدی چندین بار اجرا میشود، مانند تابع ()getNextState ما، تفسیر عملکرد خود را از دست میدهد، زیرا باید قطعه کد یکسانی را به طور مکرر تفسیر کند و زمانی که این اتفاق بیافتد profiler این قطعه کد را به صورت «کد گرم» (Warm Code) علامتگذاری میکند و «کامپایلر مبنا» (Baseline Compiler) وارد عمل میشود.
کامپایلر مبنا
برای این که طرز کار JIT را بهتر نشان دهیم از این پس از قطعه کد زیر به عنوان مثال استفاده میکنیم:
زمانی که پروفایلر یک قطعه کد را به صورت «کد گرم» علامتگذاری میکند، JIT کد را به کامپایلر مبنا میسپارد که یک کد کامپایل شده میسازد و در همین حال پروفایلر همچنان به گردآوری دادهها در ارتباط با فراوانی و انواع کدهای اجرا شده ادامه میدهد. زمانی که این بخش از کد اجرا میشود (در مثال فرضی ما بخش ;return x + y است) JIT تنها کافی است این بخش کامپایل شده را مجدداً اجرا کند. زمانی که کد گرم چندین بار به روش مشابه فراخوانی شود، به صورت «کد داغ» (hot code) علامتگذاری میشود.
کامپایلر بهینهساز
زمانی که یک قطعه کد به صورت کد داغ علامتگذاری شود، «کامپایلر بهینهساز» (Optimizer Compiler) یک نسخه باز هم سریعتر از این کد میسازد. این وضعیت تنها بر مبنای این فرضیه عمل میکند که کامپایلر بهینهساز، نوع متغیرها یا شکل شیءهای مورد استفاده در کد را بهینهسازی میکند. در مورد مثال فرضی ما میتوان تصور کرد که «کد داغ» ;return x + y هر دو مقدار x و y را به صورت number فرض میکند.
مشکل این است که در مواردی کد با چیزی مواجه میشود که کامپایلر بهینهساز انتظار ندارد، برای نمونه در مورد مثال ما با ('sum(15, '6 فراخوانی میشود، چون y یک string است. زمانی که این اتفاق میافتد، پروفایلر فرض میکند که فرضیات آن اشتباه بوده است و همه چیز را کنار گذاشته و به نسخه کامپایل شده مبنا (یا تفسیری) باز میگردد. این مرحله «غیر بهینهسازی» (Deoptimization) نام دارد. برخی اوقات این اتفاق چنان مکرر رخ میدهد که حتی نسخه بهینه شده نسبت به نسخه مبنا کندتر میشود.
جمعبندی
برخی موتورهای جاوا اسکریپت در خصوص کمّیت تلاشهای بهینهسازی محدودیتهایی دارند و زمانی که به این حد برسند دیگر برای بهینهسازی تلاش نمیکنند. برخی دیگر مانند V8 به صورت شهودی زمانی که میبینند احتمالاً کد «غیربهینهسازی» خواهد شد از بهینهسازی آن اجتناب میکنند. این فرایند bailing out نام دارد.
بنابراین به طور خلاصه مراحل کامپایلر JIT را میتوان به صورت زیر توصیف کرد:
- تجزیه
- کامپایل
- بهینهسازی/غیر بهینه سزی
- اجرا
- Garbage Collector

همه این پیشرفتها که به وسیله کامپایلرهای JIT ارائه شده است، موجب گشته که جاوا اسکریپت نسبت به سال 2008 بسیار سریعتر شود. اپلیکیشنهای امروزی به لطف سرعت موتورهای جاوا اسکریپت بسیار پایدارتر هستند.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای مهندسی نرم افزار
- آموزش طراحی کامپایلر
- آموزش طراحی کامپایلر (مرور و حل تست های کنکور کارشناسی ارشد)
- کامپایلر، طراحی و معماری آن — به زبان ساده
- تولید کد میانی (Intermediate Code) در طراحی کامپایلر – راهنمای جامع
- آموزش طراحی کامپایلر — مجموعه مقالات جامع وبلاگ فرادرس
==
برای کامپایل زبانهای سطح بالایی مانند C/C++/Rust
این قسمت اشتباهی نوشتید زبان های سطح بالایی در صورتی که این زبان سطح پایینی هستند
با عرض سلام و وقت بخیر؛
سپاس از دقت نظر شما، این مورد اصلاح شد.
از همراهی شما با مجله فرادرس بسیار سپاسگزاریم.