00001001

مترجمان نامرئی
سفر شگفتانگیز کدهای برنامهنویسی به دنیای صفر و یک
۶ دقیقه مطالعه
مطالب این شماره
فهرست مطلب
وقتی کدی را به زبانهای محبوبی مانند JavaScript ،Go ،Python یا Rust مینویسیم، معمولاً درگیر این پیچیدگی نمیشویم که این خطوط خوانا برای انسان، چگونه به زبان ماشین (صفر و یک) تبدیل میشوند تا پردازنده بتواند آنها را درک و اجرا کند. در گذشتۀ دنیای نرمافزار، اگر کسی میخواست یک زبان برنامهنویسی جدید خلق کند، با یک کابوس تمامعیار روبهرو بود: او باید برای تکتک معماریهای سختافزاری موجود (مثل ARM ،Intel x86 یا RISC-V) یک مبدل یا جداگانه به زبان Assembly مینوشت؛ این یعنی اختراع دوبارۀ چرخ از ابتدا، برای هر پردازنده!
اما ظهور پروژۀ شگفتانگیزی به نام LLVM این معادلات را برای همیشه تغییر داد. LLVM در واقع یک موتور کامپایلر بهشدت ماژولار است که فرایند پیچیدۀ کامپایل را به سه مرحلۀ کاملاً مستقل تقسیم کردهاست.
معماری سهلایۀ LLVM: کارخانهٔ تبدیل کد
-
لایۀ اول - : در این مرحله، کدهای زبان برنامهنویسی شما خوانده میشوند. ساختار گرامری آنها پردازش شده و بهصورت «درخت نحو انتزاعی» یا درمیآید. در نهایت، این ساختار به یک زبان استاندارد و سطح پایین میانی به نام ترجمه میشود. معروفترین فرانتاند این اکوسیستم برای زبانهای C و ++C، ابزاری به نام Clang است.
-
لایۀ دوم - : حالا کدهای میانی وارد یک ماشین جادویی میشوند. این بخش اصلاً اهمیتی نمیدهد که کد اولیۀ شما با چه زبانی نوشته شده بود یا قرار است روی چه پردازندهای اجرا شود. بهینهساز با استفاده از مجموعهای از الگوریتمهای پیچیده به نام Passes، سرعت اجرای کد را بهشدت بالا میبرد؛ را حذف میکند، و حالت بهینۀ منطقی برنامه را شکل میدهد.
-
لایۀ سوم - : در ایستگاه آخر، کد بهینهشده تحویل گرفته میشود و با در نظر گرفتن محدودیتهای دقیق سختافزاری (مانند تخصیص رجیسترهای پردازنده و cache)، دقیقاً به زبان Assembly مخصوص برای معماری مقصد تبدیل میشود.
این معماری سهلایه باعث شد تا خالقان زبانهای مدرنی مثل Rust یا Swift، دیگر نیازی به اختراع دوبارۀ چرخ از ابتدا نداشته باشند؛ آنها فقط کافی است کدهای زبانشان را (بهعنوان فرانتاند) به LLVM IR تبدیل کنند و بقیۀ کارهای سنگین، بهینهسازیها و تولید کد ماشین را به موتور قدرتمند LLVM بسپارند.
نبرد غولها: LLVM در برابر GCC و ماجرای GIMPLE
بسیاری از افراد بهاشتباه تصور میکنند که کامپایلر قدیمی و قدرتمندی مثل GCC هم در لایههای زیرین خود از LLVM استفاده میکند؛ در حالیکه این دو، رقیب مستقیم یکدیگرند. دلیل این سوء تفاهم این است که
فرانتاند LLVM (یعنی Clang) عمداً طوری طراحی شده که فلگها و دستورات ترمینال آن کاملاً مشابه GCC باشد تا توسعهدهندگان بتوانند بهراحتی و بدون دستکاری تنظیمات پروژهها، بین این دو کامپایلر جابهجا شوند.
در واقع، GCC موتور و زبان واسط اختصاصی خودش را دارد که به آن GIMPLE میگویند. یک تفاوت بسیار مهم و بنیادین بین LLVM و GCC همینجا مشخص میشود: پایداری .
معماری LLVM بهعنوان یک ابزار عمومی و ماژولار طراحی شدهاست؛ اما در سمت مقابل، API های GIMPLE در GCC اصلاً پایدار نیستند.
به این معنا که کد یا پلاگینی که امروز برای کار با GIMPLE در GCC نسخۀ ۱۰ ساخته شدهاست، هیچ تضمینی ندارد که در GCC نسخۀ ۱۱ کار کند. دلیل آن هم این است که توسعهدهندگان GCC، این زبان واسط را بهعنوان یک محصول عمومی برای توسعهدهندگان خارجی ارائه نکردهاند، بلکه هدفشان صرفاً استفادۀ داخلی برای خود کامپایلر GCC بودهاست.
جهان موازی زبانهای واسط (IR)
دنیای زبانهای واسط به LLVM ختم نمیشود. بسته به نیازهای پردازشی دنیای مدرن، تکنولوژیهای بینظیر دیگری نیز خلق شدهاند که در ادامه میبینید.
| نام تکنولوژی (زبان واسط) | کاربرد و ویژگی اصلی |
|---|---|
| گوگل متوجه شد که LLVM IR برای پردازشهای موازی روی کارت گرافیک () و کاربردهای هوش مصنوعی بسیار سطح پایین است. به همین دلیل، نسل جدیدی به نام MLIR خلق شد که فریمورکهایی مثل TensorFlow برای پردازش سریعتر تنسورها از آن استفاده میکنند. | |
| WebAssembly (Wasm) | برای اجرای مستقیم کدهای سنگین با سرعتی نزدیک به زبان بومی در مرورگرها ساخته شد و مرزهای پردازشی وباپلیکیشنها را جابهجا کرد. |
| Java Bytecode & | زبانهای باسابقهای مثل Java و خانوادهٔ NET. سالهاست برای رسیدن به شعار «یک بار بنویس، همهجا اجرا کن»، کدهای خود را بهجای سختافزار واقعی، برای ماشینهای مجازی ( و ) کامپایل میکنند. |
زبانهای مدرن چگونه کدهای ما را میبلعند؟
اگر با جزئیات بیشتری به مسیر اجرای زبانهای پرکاربرد امروزی نگاه کنیم، داستان بسیار جذابتر میشود. هر زبان با توجه به فلسفۀ وجودیاش، مسیر کاملاً متفاوتی را برای تبدیل شدن به کد ماشین طی میکند.
-
Rust - وسواس در امنیت حافظه: این زبان بهشدت روی امنیت مموری حساس است و اجازه نمیدهد برنامۀ شما خطای Segmentation Fault بدهد. کامپایلر Rust ابتدا کدها را به زبانهای واسط داخلی و سطح بالای خودش به نامهای و تبدیل میکند. در این لایهها با دقت بسیار زیاد، قوانین سختگیرانۀ مالکیت و مدیریت مموری را بررسی میکند. وقتی خیالش راحت شد که هیچ خطایی در مدیریت حافظه نیست، آنها را به LLVM IR تبدیل میکند و به موتور LLVM میدهد.
-
Go - جنون سرعت در کامپایل: زبان Go با کارایی بالایش مسیر کاملاً متفاوتی را طی کرده و اصلاً از LLVM استفاده نمیکند! تیم گوگل برای زبان Go، یک کامپایلر اختصاصی از صفر نوشتهاست که کدها را به یک فرمت واسط به نام تبدیل میکند و خودش بهطور مستقیم کدهای اسمبلی را تولید میکند. دلیل این تصمیم، این بوده که یکی از اهداف اصلی سازندگان Go «سرعت بسیار بالای زمان کامپایل» بودهاست؛ و کامپایلر اختصاصی Go در این زمینه به مراتب از LLVM (که ابزار سنگین و حجیمی است) سریعتر عمل میکند.
-
Python - مفسر کلاسیک: پایتون استاندارد (CPython) ابتدا کدهای شما را کرده و به فایلهای بایتکد تبدیل میکند. سپس این بایتکدها درون یک ماشین مجازی در یک حلقۀ بینهایت، خطبهخط تفسیر و اجرا میشوند. همین واسطۀ نرمافزاری باعث کندتر بودن پایتون در پردازشهای سنگین محاسباتی میشود؛ مگر اینکه از ابزارهایی مثل Numba استفاده کنید که با قدرت LLVM، کدهای پایتون را به کد ماشین سریع تبدیل میکنند.
-
JavaScript - جادوی کامپایل درجا: جاوا اسکریپت هم ابتدا به بایتکد تبدیل میشود، اما در مرورگرها از موتورهای بهشدت پیشرفتهای مثل V8 (در Chrome) یا SpiderMonkey (در Firefox) استفاده میکند. جالب است بدانید SpiderMonkey تاریخچهای حماسی دارد و اولین موتور جاوا اسکریپت تاریخ است که خالق این زبان برای مرورگر Netscape نوشت و بعداً Mozilla آن را توسعه داد.
هر دوی این موتورها مجهز به تکنیکی به نام کامپایل درجا هستند. موتور Chrome در پسزمینه، کدهایی را که بهطور مداوم در حال تکرار هستند (معروف به HotSpots) شناسایی میکند؛ سپس بدون اینکه اجرای برنامه متوقف شود، همان لحظه آن قطعههای کد را توسط کامپایلری بهنام TurboFan بهطور مستقیم به کد ماشین بهینه تبدیل میکند. در Firefox نیز دقیقاً همین اتفاق میافتد، با این تفاوت که از کامپایلرهای اختصاصی خودش به نامهای WarpMonkey یا IonMonkey استفاده میکند تا کارایی وباپلیکیشنها را به میزان شگفتانگیزی افزایش دهد.
سخن آخر
این موتورهای نامرئی و زبانهای واسط هستند که بیوقفه افکار مهندسی ما را برای پردازندهها ترجمه میکنند. بدون وجود این معماریهای چندلایه و مهندسیشده، توسعه، بهینهسازی و اجرای نرمافزارهای مدرن عملاً ناممکن بود.