راز سرعت راست کجاست؟

راز سرعت راست کجاست؟

چگونگی دوگانه سرعت و امنیت در rust محبوب

راست: زبانی که ارگونومی سطح بالا را با سرعت C ترکیب کرد

برای دهه‌ها، برنامه‌نویسان بین دو دنیای جدا از هم انتخاب می‌کردند: زبان‌های سطح پایین مثل C و ++C که کنترل کامل و سرعت خام سخت‌افزار را می‌دادند اما مدیریت حافظه را به عهده‌ی خود برنامه‌نویس می‌گذاشتند، یا زبان‌های سطح بالا مثل Python و Java که نوشتن کد را ساده و ایمن می‌کردند اما هزینه‌ی این سادگی، لایه‌ای اضافی از سربار اجرایی (Runtime Overhead) بود. رست (Rust) یکی از معدود زبان‌هایی است که توانسته این دوگانگی تاریخی را بشکند. در این مقاله بررسی می‌کنیم رست چگونه از نظر فنی به این ترکیب دست پیدا کرده است.

  • مشکل اصلی: چرا زبان‌های ایمن معمولاً کند هستند؟

زبان‌هایی مثل Python، Java یا C# برای این‌که برنامه‌نویس را از مدیریت دستی حافظه راحت کنند، از جمع‌آوری زباله (Garbage Collector یا GC) استفاده می‌کنند. GC به‌صورت دوره‌ای اجرا می‌شود، حافظه‌ی استفاده‌نشده را شناسایی و آزاد می‌کند، اما این کار هزینه‌ی محاسباتی و زمانی دارد؛ برنامه گاهی برای مدت کوتاهی متوقف می‌شود (Stop-the-world pause) تا GC کارش را انجام دهد. علاوه بر این، بسیاری از این زبان‌ها از ماشین مجازی یا مفسر (Interpreter) استفاده می‌کنند که خود یک لایه‌ی میانی اضافی بین کد و سخت‌افزار ایجاد می‌کند.

از سوی دیگر، C و ++C این سربار را ندارند چون کامپایل مستقیم به کد ماشین انجام می‌شود و مدیریت حافظه کاملاً دستی است، اما همین آزادی، منبع اصلی باگ‌های امنیتی و پایداری در طول تاریخ نرم‌افزار بوده است: دسترسی به حافظه‌ی آزادشده (Use-after-free)، سرریز بافر (Buffer Overflow) و نشتی حافظه (Memory Leak) از رایج‌ترین مشکلات این زبان‌ها هستند.

  • راه‌حل رست: مالکیت (Ownership) به‌جای جمع‌آوری زباله

نوآوری اصلی رست، سیستمی به نام مالکیت (Ownership) است که در زمان کامپایل، بدون نیاز به هیچ runtime یا garbage collector، ایمنی حافظه را تضمین می‌کند. قوانین اصلی این سیستم ساده اما قدرتمند هستند:

  • هر مقدار در حافظه دقیقاً یک مالک (Owner) دارد.
  • وقتی مالک از محدوده‌ی خود (Scope) خارج می‌شود، حافظه بلافاصله و به‌صورت خودکار آزاد می‌شود.
  • مالکیت می‌تواند بین متغیرها منتقل (Move) شود، اما در هر لحظه فقط یک مالک معتبر وجود دارد.

این مکانیزم به‌جای بررسی حافظه در زمان اجرا (Runtime)، تمام این بررسی‌ها را در زمان کامپایل (Compile Time) انجام می‌دهد. به همین دلیل، برنامه‌ی نهایی هیچ سربار اضافه‌ای برای مدیریت حافظه در زمان اجرا ندارد؛ دقیقاً همان چیزی که در C دستی نوشته می‌شود، اما این‌جا کامپایلر آن را تضمین می‌کند.

  • قرض‌گیری و بررسی‌کننده‌ی قرض (Borrow Checker)

از آن‌جا که در هر لحظه فقط یک مالک برای یک مقدار مجاز است، رست مفهومی به نام قرض‌گیری (Borrowing) را معرفی کرده تا بتوان بدون انتقال مالکیت، به داده دسترسی موقت داشت. کامپایلر رست شامل بخشی به نام Borrow Checker است که در زمان کامپایل تضمین می‌کند:

  • یا می‌توانید چند اشاره‌گر فقط‌خواندنی (Immutable Reference) هم‌زمان داشته باشید،
  • یا فقط یک اشاره‌گر قابل‌تغییر (Mutable Reference)،

اما هرگز این دو حالت هم‌زمان اتفاق نمی‌افتد. این قانون ساده، دسته‌ی بزرگی از باگ‌های همزمانی (Data Race) و خطاهای دسترسی به حافظه را کاملاً از ریشه حذف می‌کند، بدون این‌که هیچ بررسی اضافه‌ای در زمان اجرا لازم باشد.

  • انتزاع‌های بدون‌هزینه: فلسفه‌ی Zero-Cost Abstractions

یکی از اصول بنیادین طراحی رست، مفهومی به نام انتزاع بدون‌هزینه (Zero-Cost Abstraction) است. این فلسفه می‌گوید: ویژگی‌های سطح بالا (مثل iteratorها، closureها، generics و pattern matching) باید کد خواناتر و ایمن‌تری تولید کنند، اما در نهایت باید به کد ماشینی تبدیل شوند که از نظر عملکرد دقیقاً معادل کدی باشد که یک برنامه‌نویس با تجربه مستقیماً در C می‌نوشت.

برای مثال، وقتی از یک iterator برای پیمایش یک آرایه استفاده می‌کنید، کامپایلر رست معمولاً آن را به همان حلقه‌ی ساده و بهینه‌ای تبدیل می‌کند که با دست در C می‌نوشتید، بدون فراخوانی تابع اضافه یا سربار شیءگرایی. این یعنی می‌توانید کد خوانا و سطح بالا بنویسید، بدون این‌که هزینه‌ی عملکردی برای آن بپردازید.

  • زیرساخت کامپایلر: LLVM و کد ماشین بهینه

رست از LLVM به‌عنوان زیرساخت بهینه‌سازی و تولید کد ماشین استفاده می‌کند؛ همان زیرساختی که کامپایلرهای ++Clang/C نیز از آن بهره می‌برند. این یعنی رست از تمام تکنیک‌های بهینه‌سازی پیشرفته‌ای که سال‌ها روی LLVM توسعه داده شده — مثل بهینه‌سازی حلقه، inlining توابع و بردارسازی (Vectorization) — به‌طور مستقیم بهره می‌برد و کد نهایی آن از نظر عملکرد در بسیاری از بنچمارک‌ها با C و ++C برابری می‌کند.

  • طول عمر (Lifetimes): اثبات ایمنی مراجع در زمان کامپایل

سیستم قرض‌گیری به‌تنهایی کافی نیست؛ کامپایلر باید مطمئن شود که یک مرجع (Reference) هرگز به داده‌ای اشاره نمی‌کند که از حافظه پاک شده است (مشکلی معروف به Dangling Pointer در C). رست این مسئله را با مفهوم طول عمر (Lifetime) حل می‌کند. هر مرجع در رست به‌صورت ضمنی یا صریح دارای یک طول عمر است که کامپایلر آن را از طریق الگوریتمی به نام Borrow Checker با تحلیل جریان کنترل (Control-Flow-based Region Inference) استخراج و اعتبارسنجی می‌کند.

برای مثال در امضای تابع fn longest<'a>(x: &'a str, y: &'a str) -> &'a str، پارامتر عمومی 'a به کامپایلر می‌گوید که مقدار بازگشتی نمی‌تواند بیشتر از کوتاه‌ترین طول عمر بین دو ورودی زنده بماند. این بررسی کاملاً در زمان کامپایل و بدون تولید هیچ کد اضافه در باینری نهایی انجام می‌شود؛ برخلاف زبان‌هایی که برای این منظور از شمارنده‌ی ارجاع (Reference Counting) در زمان اجرا استفاده می‌کنند.

  • مونومورفیزاسیون: چرا Genericها در رست سربار ندارند

وقتی از Genericها در رست استفاده می‌کنید (مثلاً یک تابع که روی هر نوع T کار می‌کند)، کامپایلر به‌جای تولید یک نسخه‌ی عمومی که در زمان اجرا نوع را بررسی کند، فرایندی به نام مونومورفیزاسیون (Monomorphization) انجام می‌دهد: برای هر نوع مشخصی که واقعاً در برنامه استفاده شده، یک نسخه‌ی جداگانه و تخصصی‌شده از تابع تولید می‌کند. اگر یک تابع generic هم با i32 و هم با f64 فراخوانی شود، کامپایلر دو نسخه‌ی کاملاً مجزا و بهینه از آن تابع می‌سازد، دقیقاً مشابه چیزی که اگر خودتان دو تابع جداگانه در C می‌نوشتید تولید می‌شد.

این رویکرد در تضاد مستقیم با dispatch پویا (Dynamic Dispatch) در زبان‌هایی مثل Java است، جایی که تعیین نوع واقعی در زمان اجرا و از طریق جدول متدهای مجازی (Vtable) انجام می‌شود و هزینه‌ی یک jump غیرمستقیم اضافه به عملیات تحمیل می‌کند. رست این dispatch پویا را هم پشتیبانی می‌کند (از طریق trait objectها مثل dyn Trait)، اما این حالت اختیاری است؛ پیش‌فرض رست همیشه dispatch ایستا (Static Dispatch) و بدون سربار زمان اجراست.

  • پشته در مقابل هیپ: کنترل دقیق محل تخصیص حافظه

بر خلاف زبان‌هایی مثل Python یا Java که تقریباً همه‌چیز روی heap تخصیص می‌یابد و توسط GC مدیریت می‌شود، رست به برنامه‌نویس اجازه می‌دهد دقیقاً تعیین کند چه داده‌ای روی پشته (Stack) و چه داده‌ای روی هیپ (Heap) قرار می‌گیرد. انواع داده با اندازه‌ی مشخص در زمان کامپایل (مثل اعداد، آرایه‌های ثابت و structها) به‌طور پیش‌فرض روی پشته قرار می‌گیرند که تخصیص و آزادسازی آن‌ها صرفاً جابه‌جایی اشاره‌گر پشته است؛ عملیاتی تقریباً بدون هزینه. تنها زمانی که صریحاً از انواعی مثل Box<T>، Vec<T> یا String استفاده می‌کنید، تخصیص روی هیپ اتفاق می‌افتد. این شفافیت کامل، همان کنترلی است که در C وجود دارد اما در زبان‌های مدیریت‌شده معمولاً از دید برنامه‌نویس پنهان است.

  • RAII و trait Drop: آزادسازی قطعی و قابل‌پیش‌بینی منابع

رست از الگوی RAII (Resource Acquisition Is Initialization) که ریشه در ++C دارد استفاده می‌کند. هر مقدار در رست می‌تواند یک پیاده‌سازی از trait به نام Drop داشته باشد که دقیقاً در لحظه‌ای که مالک آن مقدار از scope خارج می‌شود، به‌طور خودکار و قطعی فراخوانی می‌شود. برخلاف GC که زمان دقیق پاکسازی را تضمین نمی‌کند (finalization غیرقطعی)، در رست می‌دانید دقیقاً در کدام خط از کد، destructor اجرا خواهد شد. این ویژگی برای مدیریت منابعی مثل فایل‌ها، اتصالات شبکه یا قفل‌های mutex حیاتی است، چون تضمین می‌کند این منابع همیشه و در زمان مشخص آزاد می‌شوند.

  • unsafe: دریچه‌ی خروج کنترل‌شده به دنیای C

رست می‌داند که برخی عملیات سطح پایین (مثل کار مستقیم با اشاره‌گرهای خام، فراخوانی توابع C از طریق FFI، یا نوشتن ساختارهای داده‌ی سفارشی مثل لینک‌لیست دوطرفه) در چارچوب سخت‌گیرانه‌ی Borrow Checker قابل بیان نیستند. به همین دلیل بلوک unsafe وجود دارد که به برنامه‌نویس اجازه می‌دهد به‌صورت محدود و آگاهانه، از برخی قوانین ایمنی عبور کند. نکته‌ی مهم این است که کد unsafe در رست هنوز هم به همان نحو کامپایل و بهینه می‌شود، بدون هیچ سربار اضافه؛ فقط مسئولیت اثبات صحت آن از کامپایلر به برنامه‌نویس منتقل می‌شود. این طراحی تضمین می‌کند که ۹۹ درصد کد یک برنامه می‌تواند کاملاً ایمن و بدون هیچ ریسکی باشد، در حالی که هسته‌ی حیاتی و سطح پایین آن هنوز هم می‌تواند مستقیماً و بدون واسطه با سخت‌افزار یا کتابخانه‌های C تعامل داشته باشد.

  • همزمانی ایمن بدون هزینه‌ی اضافه

یکی از دستاوردهای مهم رست، مدیریت همزمانی (Concurrency) بدون قربانی‌کردن ایمنی است. در بسیاری از زبان‌های دیگر، نوشتن کد چندنخی (Multi-threaded) ایمن نیازمند قفل‌ها (Locks)، ابزارهای تحلیل در زمان اجرا یا حتی محدودیت‌هایی مثل Global Interpreter Lock در Python است. در رست، همان سیستم مالکیت و قرض‌گیری که ایمنی حافظه را تضمین می‌کند، در زمان کامپایل از بروز Data Race در کد چندنخی هم جلوگیری می‌کند؛ بدون نیاز به قفل‌های سنگین یا بررسی‌های اضافه در زمان اجرا.

  • نتیجه‌ی این ترکیب در دنیای واقعی

این طراحی باعث شده رست در پروژه‌هایی که هم به عملکرد بالا و هم به ایمنی نیاز دارند، جایگاه ویژه‌ای پیدا کند: از موتورهای مرورگر (مثل بخش‌هایی از Firefox) گرفته تا ابزارهای سیستم‌عامل، پایگاه‌داده‌های پرسرعت و حتی بخش‌هایی از زیرساخت‌های ابری شرکت‌های بزرگ فناوری. توسعه‌دهندگانی که پیش‌تر مجبور بودند بین نوشتن کد ایمن یا کد سریع یکی را انتخاب کنند، اکنون با رست می‌توانند هر دو را هم‌زمان داشته باشند.

  • جمع‌بندی

رست با ترکیب سیستم مالکیت و قرض‌گیری در زمان کامپایل، فلسفه‌ی انتزاع بدون‌هزینه، و بهره‌گیری از زیرساخت بهینه‌سازی LLVM، توانسته دیوار قدیمی میان «زبان‌های ایمن و ساده» و «زبان‌های سریع و خطرناک» را بردارد. حاصل این طراحی، زبانی است که اجازه می‌دهد کد سطح بالا و خوانا بنویسید، در حالی که کامپایلر آن را به کدی تبدیل می‌کند که از نظر سرعت با C رقابت می‌کند؛ بدون نیاز به garbage collector و بدون قربانی‌کردن ایمنی حافظه.