مقایسه‌ی سلام و Rust: سرعت، مدیریت حافظه، ایمنی و کنترل سطح پایین

Rust ایمنی حافظه را در زمان کامپایل اثبات می‌کند و سلام نه. این مقایسه نشان می‌دهد هر زبان چه چیزی را بررسی می‌کند، برنامه‌های یکسان را به هر دو زبان می‌آورد، سرعت اندازه‌گیری‌شده را می‌گوید و جای مناسب هر کدام را نشان می‌دهد.

در این مقاله
  1. مقایسه‌ی سلام و Rust در یک نگاه
  2. یک برنامه به هر دو زبان
  3. مدیریت خطا: Result و ? در برابر خطادار و تلاش
  4. حافظه: بررسی‌گر قرض در برابر آزادسازی دستی
  5. سرعت: سلام در برابر Rust
  6. کنترل سطح پایین و همکاری با C
  7. ابزارها، منحنی یادگیری و اکوسیستم
  8. کی Rust و کی زبان سلام را انتخاب کنیم؟
  9. پرسش‌های پرتکرار

زبان برنامه‌نویسی سلام و Rust هر دو کامپایلی‌اند، گونه‌های ایستا دارند، زباله‌روب (Garbage Collector) ندارند، جداشمارهایی (Enum) دارند که داده با خود می‌برند، تطبیق الگوی جامع (Exhaustive Pattern Matching) دارند و به سرعت بومی می‌رسند. تفاوت تعیین‌کننده، ایمنی حافظه است: Rust یک بررسی‌گر قرض (Borrow Checker) دارد که در زمان کامپایل اثبات می‌کند کد امن هیچ استفاده پس از آزادسازی، آزادسازی دوباره و رقابت داده (Data Race) ندارد. سلام چنین چیزی ندارد. سلام حافظه‌ی هیپ را با .آزادکن() و دیرکن به دست آزاد می‌کند، مجموعه‌ای از اشتباه‌های رایج را با بررسی‌ها و هشدارهای زمان کامپایل می‌گیرد و در ساخت‌های توسعه مرز اندیس‌ها را هنگام اجرا بررسی می‌کند. این بررسی‌ها کمک می‌کنند، ولی اثبات نیستند.

در عوض، سلام زبانی کوچک‌تر با منحنی یادگیری ملایم‌تر است، کتابخانه‌ی استاندارد بزرگی همراه خود دارد و کدش را می‌توانید به انگلیسی یا فارسی بنویسید. این مقایسه‌ی سلام و Rust برنامه‌های یکسان را به هر دو زبان نشان می‌دهد و سود و زیان‌ها را بی‌پرده می‌گوید.

مقایسه‌ی سلام و Rust در یک نگاه

Rustسلام
مدیریت حافظهمالکیت، قرض و طول عمر (Lifetime) که کامپایلر بررسی می‌کند؛ Drop خودکار آزاد می‌کنددستی: .آزادکن()، معمولاً با دیرکن؛ هشدارهای زمان کامپایل برای اشتباه‌های رایج
تضمین ایمنی حافظهبله، برای کد امن؛ بلوک‌های unsafe از آن بیرون می‌روندبدون تضمین؛ بررسی مرز اندیس در ساخت توسعه، بررسی مالکیت، AddressSanitizer با salam memcheck
رقابت دادهدر زمان کامپایل با Send و Sync جلوگیری می‌شودبر عهده‌ی شما: قفل‌ها (Mutex)، عملیات اتمی، کانال‌ها
گونه‌های جمع و تطبیق الگوenum با داده، match جامعجداشمار با داده، همخوان جامع
خطاهاResult<T, E> همراه با ?خطادار، تلاش، گرفتن خطا؛ یا جداشمار Result خودتان با ? پسوندی
برنامه‌نویسی عامTraitها و عام‌های تک‌ریخت‌شده (Monomorphized)میانجی‌ها و عام‌ها با قید
تولید کدLLVM (پشتیبان‌های دیگر در دست ساخت)LLVM، C یا پشتیبان بومی خود سلام
اکوسیستم بسته‌هاcrates.io، بسیار بزرگکتابخانه‌ی استاندارد بزرگ، بسته‌های بیرونی اندک
پایدارینسخه‌ی ۱٫۰ در ۲۰۱۵؛ ویرایش‌ها (Editions) کد قدیمی را کامپایل‌پذیر نگه می‌دارندپیش از ۱٫۰؛ زبان هنوز تغییر می‌کند
منحنی یادگیریتند، به‌ویژه بررسی‌گر قرضملایم: از نظر حس و حال به Go یا پایتون نزدیک‌تر

یک برنامه به هر دو زبان

هر دو زبان گونه‌های جمع (Sum Type) دارند: جداشماری که هر عضوش فیلدهای خودش را دارد و با تطبیق الگو باز می‌شود. این یک ماشین‌حساب کوچک مساحت به Rust است:

Rust
enum Shape {
    Triangle { base: f64, height: f64 },
    Rect { w: f64, h: f64 },
    Square { side: f64 },
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Triangle { base, height } => 0.5 * base * height,
            Shape::Rect { w, h } => w * h,
            Shape::Square { side } => side * side,
        }
    }

    fn name(&self) -> &'static str {
        match self {
            Shape::Triangle { .. } => "Triangle",
            Shape::Rect { .. } => "Rect",
            Shape::Square { .. } => "Square",
        }
    }
}

fn main() {
    let shapes = vec![
        Shape::Triangle { base: 4.0, height: 3.0 },
        Shape::Rect { w: 3.0, h: 4.5 },
        Shape::Square { side: 5.0 },
    ];
    let mut total = 0.0;
    for s in &shapes {
        println!("{} area {}", s.name(), s.area());
        total += s.area();
    }
    println!("total: {}", total);
}

و به سلام فارسی:

سلام
جداشمار شکل:
    مثلث(قاعده: اعشار۶۴، ارتفاع: اعشار۶۴)
    مستطیل(پهنا: اعشار۶۴، بلندی: اعشار۶۴)
    مربع(ضلع: اعشار۶۴)
پایان

تابع مساحت(ش: شکل): اعشار۶۴:
    برگشت همخوان ش:
        مثلث(ق، ا): ۰.۵ * ق * ا پایان
        مستطیل(پ، ب): پ * ب پایان
        مربع(ض): ض * ض پایان
    پایان
پایان

تابع ریشه:
    ناپایا شکل‌ها := وکتور {} برگردان وکتور<شکل>
    دیرکن شکل‌ها.آزادکن()
    شکل‌ها.بیفزا(شکل.مثلث(۴.۰، ۳.۰))
    شکل‌ها.بیفزا(شکل.مستطیل(۳.۰، ۴.۵))
    شکل‌ها.بیفزا(شکل.مربع(۵.۰))
    ناپایا مجموع := ۰.۰
    هر ش در شکل‌ها:
        سرچاپ ش.نام()، "مساحت"، مساحت(ش)
        مجموع += مساحت(ش)
    پایان
    سرچاپ "مجموع:"، مجموع
پایان
خروجیمثلث مساحت 6 مستطیل مساحت 13.5 مربع مساحت 25 مجموع: 44.5

هر دو همان عددها را چاپ می‌کنند. در هر دو زبان، حذف یکی از شاخه‌های تطبیق خطای کامپایل است، چون کامپایلر بررسی می‌کند که همه‌ی عضوها پوشش داده شده باشند (درس جداشمارها نسخه‌ی سلام را توضیح می‌دهد). تفاوت‌های پیدا کوچک‌اند: مقدارهای جداشمار در سلام یک متد درونی نام() (در انگلیسی name()) دارند که نام عضو را، حتی اگر فارسی باشد، برمی‌گرداند، و سلام به دیرکن شکل‌ها.آزادکن() نیاز دارد، در حالی که Vec در Rust با بیرون رفتن از دامنه خودش آزاد می‌شود.

مدیریت خطا: Result و ? در برابر خطادار و تلاش

Rust خطاها را به شکل مقدارهایی از گونه‌ی Result<T, E> برمی‌گرداند و ? خطا را به فراخواننده می‌رساند:

Rust
fn parse_port(text: &str) -> Result<u16, String> {
    let n: u16 = text
        .parse()
        .map_err(|e| format!("cannot read {text:?}: {e}"))?;
    if n == 0 {
        return Err(format!("port out of range: {n}"));
    }
    Ok(n)
}

fn main() {
    for arg in ["8080", "0", "http"] {
        match parse_port(arg) {
            Ok(port) => println!("{arg} -> port {port}"),
            Err(e) => println!("{arg} -> error: {e}"),
        }
    }
}

ابزار معمول سلام تابعی است که با خطادار (در انگلیسی throws) علامت خورده باشد. تلاش خطا را به فراخواننده می‌رساند و بلوک تلاش: همراه با گرفتن خطا آن را مدیریت می‌کند. خطاها مقدارهایی از گونه‌ی خطا با نوع، پیام و کد هستند، پس این هم همچنان انتقال صریح خطاست، فقط به شکلی دیگر نوشته شده:

سلام
واردسازی تبدیل

تابع خواندن درگاه(متن: رشته): صحیح خطادار:
    ن := تلاش تبدیل.تجزیه عدد(متن)
    اگر ن < ۱ یا ن > ۶۵۵۳۵:
        پرتاب خطا "درگاه بیرون از محدوده است: " + ن
    پایان
    برگشت ن برگردان صحیح
پایان

تابع ریشه:
    هر نوشته در ["8080"، "0"، "http"]:
        تلاش:
            سرچاپ نوشته، "-> درگاه"، خواندن درگاه(نوشته)
        گرفتن خطا خ:
            سرچاپ نوشته، "-> خطا:"، خ.پیام
        پایان
    پایان
پایان
خروجی8080 -> درگاه 8080 0 -> خطا: درگاه بیرون از محدوده است: 0 http -> خطا: invalid character 'h'

صدا زدن تابع خطادار بدون تلاش کامپایل نمی‌شود (خطای E155؛ در کد فارسی با پیام «این فراخوانی می‌تواند خطا پرتاب کند: آن را با 'تلاش' علامت بزنید تا خطا به بالا برسد، یا درون یک بلوک 'تلاش:' بگذارید تا آن را مدیریت کنید»)، پس خطا را نمی‌شود بی‌صدا نادیده گرفت. Rust اثر مشابهی را با #[must_use] روی Result به دست می‌آورد، البته به شکل هشدار، نه خطا. اگر سبک Rust را ترجیح می‌دهید، سلام اجازه می‌دهد خودتان enum Result<T, E>: Ok(value: T), Err(error: E) end را تعریف کنید، و ? پسوندی روی چنین مقداری، مانند Rust، خطا را زودتر برمی‌گرداند. نوشته‌ی گونه‌های ایستا در سلام گونه‌های اختیاری T?، جداشمارها و هر دو سبک مدیریت خطا را بررسی می‌کند.

حافظه: بررسی‌گر قرض در برابر آزادسازی دستی

این مهم‌ترین بخش این مقایسه است، پس ارزش دارد دقیق باشیم.

در Rust هر مقدار یک مالک دارد، ارجاع‌ها طوری بررسی می‌شوند که هرگز بیشتر از چیزی که به آن اشاره می‌کنند عمر نکنند، و حافظه وقتی مالکش از دامنه بیرون برود خودکار آزاد می‌شود. استفاده از مقداری که منتقل شده رد می‌شود:

Rust
fn main() {
    let names = vec![String::from("Sara")];
    drop(names);
    println!("{}", names.len()); // error[E0382]: borrow of moved value: `names`
}

در سلام، آزادسازی یک فراخوانی صریح است و کامپایلر بخشی از آنچه پس از آن رخ می‌دهد را دنبال می‌کند. استفاده از یک اشاره‌گر خام پس از حافظه.آزادسازی (در انگلیسی mem.Free) یک خطای قطعی است:

سلام
واردسازی حافظه

تابع ریشه:
    پ := حافظه.تخصیص(۱۶ برگردان طبیعی۶۴) برگردان صحیح۶۴*
    پ[۰] = ۴۲
    حافظه.آزادسازی(پ برگردان تهی*)
    سرچاپ پ[۰]
پایان
متن
خطا[خطا۱۳۹]: 'پ' پس از آزاد شدن حافظه‌اش در خط ۶ استفاده شده است
 --> uaf.salam:7:11
  |
7 |     سرچاپ پ[۰]
  |           ^
  |
  = راهنما: کپی یک Vector، HashMap، str یا اشاره‌گر همان حافظهٔ heap را به اشتراک می‌گذارد؛ آن را فقط پس از آخرین استفادهٔ همهٔ کپی‌ها آزاد کنید

بررسی‌گر مالکیت این‌ها را هم گزارش می‌کند: آزاد کردن دوباره (E140)، برگرداندن چیزی که دیرکن خود تابع آزادش می‌کند (E141)، همین اشتباه‌ها وقتی فقط در برخی مسیرهای اجرا رخ می‌دهند (هشدارهای W142 و W143)، و نگه داشتن یک اشاره‌گر یا کپی در طول فراخوانی‌ای که ممکن است یک وکتور یا نگاشت را بزرگ کند و فضای آن را جابه‌جا کند (W144). هیچ هزینه‌ای در زمان اجرا ندارد، و --ownership=error هشدارهایش را به خطا تبدیل می‌کند.

اما همه‌ی چیزهایی را که بررسی‌گر Rust پوشش می‌دهد پوشش نمی‌دهد. این برنامه‌ی سلام، که همتای مستقیم نمونه‌ی Rust بالاست، امروز بدون هیچ پیامی کامپایل می‌شود:

سلام
تابع ریشه:
    ناپایا نام‌ها := وکتور {} برگردان وکتور<رشته>
    نام‌ها.بیفزا("سارا")
    نام‌ها.آزادکن()
    سرچاپ نام‌ها.طول()
پایان

در ساخت توسعه، وکتور آزادشده خالی خوانده می‌شود، پس این برنامه 0 چاپ می‌کند، و یک بگیر روی آن برنامه را هنگام اجرا با خطای مرز اندیس متوقف می‌کرد. این یک تور ایمنی است، نه تضمین زمان کامپایل. همین درباره‌ی نخ‌ها هم صادق است: هیچ چیز در سیستم گونه‌ی سلام جلوی دو نخ را نمی‌گیرد که بدون قفل در یک متغیر بنویسند، کاری که Rust با Send و Sync رد می‌کند. نوشته‌ی برنامه‌نویسی هم‌زمان در سلام ابزارهایی را که سلام به جای آن می‌دهد نشان می‌دهد.

خلاصه‌ی صادقانه: Rust جلوی این باگ‌ها را می‌گیرد؛ سلام بسیاری از آن‌ها را تشخیص می‌دهد، تعداد بیشتری را در ساخت توسعه هنگام اجرا می‌گیرد، و بقیه را به شما، به آزمون‌ها و به salam memcheck می‌سپارد. نوشته‌ی مدیریت حافظه در سلام مدل سلام را به طور کامل شرح می‌دهد.

سرعت: سلام در برابر Rust

هیچ‌کدام از این دو زبان زباله‌روب یا ماشین مجازی ندارد و هر دو ساخت‌های اصلی‌شان را از راه LLVM کامپایل می‌کنند، پس باید انتظار داشته باشید به هم نزدیک باشند. در سنجش پروژه روی ۳۹ برنامه‌ی کوچک با الگوریتم یکسان در همه‌ی زبان‌ها، میانگین سلام ۱۰٫۶ میلی‌ثانیه و میانگین Rust ۱۱٫۳ میلی‌ثانیه بود. این یعنی یک رده، نه رتبه‌بندی: بیشتر برنامه‌ها چند میلی‌ثانیه اجرا می‌شوند، راه‌اندازی بخشی از هر زمان است و فاصله‌های کمتر از حدود ۵٪ میان اجراها جابه‌جا می‌شوند. Rust در یک آزمون فشار جستجوی دودویی (۵۸۳٫۹ میلی‌ثانیه در برابر ۶۶۷٫۷) و در چاپ یک خط به تعداد یک میلیون بار (۲٫۶ میلی‌ثانیه در برابر ۸٫۲) آشکارا سریع‌تر بود، و این دو اغلب تنها چند درصد با هم فاصله داشتند.

یک تفاوت واقعی: ساخت‌های توسعه‌ی سلام بررسی مرز اندیس را نگه می‌دارند و ساخت‌های --release آن را برمی‌دارند، در حالی که Rust بررسی مرز را در ساخت انتشار هم نگه می‌دارد، مگر این‌که بهینه‌ساز ثابت کند لازم نیست یا شما از unsafe یا الگوهای پیمایشگری استفاده کنید که از آن پرهیز می‌کنند. صفحه‌ی سرعت و خوانش دقیق سنجش سرعت همه‌ی عددها و قیدها را دارند.

کنترل سطح پایین و همکاری با C

هر دو زبان تا خود سخت‌افزار پایین می‌روند. Rust اشاره‌گرهای خام درون unsafe، #[repr(C)]، extern "C" و no_std را برای هسته‌ها و سفت‌افزارها دارد. سلام اشاره‌گرها (T*، تهی*، پوچ) را همه‌جا در دسترس دارد، بلوک‌های درون‌داد: برای صدا زدن C، بلوک‌های برون‌داد: برای صدا خوردن از C، ساختارهایی که طبق ABI زبان C روی آن سکو به شکل مقدار منتقل می‌شوند، و هدف‌های مستقلی مانند x86_64-unknown-none با یک محیط اجرای کوچک درونی. اشاره‌گرهای سلام پشت واژه‌ای مانند unsafe حصار نکشیده‌اند، و این کد سطح پایین را کوتاه‌تر و در عین حال خطاپذیرتر می‌کند.

داستان Rust در سیستم‌های نهفته و هسته‌ها بسیار پخته‌تر است، با پشتیبانی در هسته‌ی لینوکس و یک گروه کاری بزرگ برای سیستم‌های نهفته. پشتیبانی سلام از برنامه‌های مستقل جوان است؛ برای آنچه امروز هست، نوشته‌ی سلام مستقل و سخت‌افزار خام را ببینید.

ابزارها، منحنی یادگیری و اکوسیستم

ابزارهای Rust عالی‌اند: Cargo، crates.io، rustfmt، Clippy، rust-analyzer و docs.rs، همراه با اکوسیستم کتابخانه‌ای بسیار بزرگ و یک دهه پایداری زیر سامانه‌ی ویرایش‌هایش. منحنی یادگیری‌اش به تندی مشهور است، بیشتر به خاطر مالکیت و طول عمرها، و زمان کامپایل در پروژه‌های بزرگ از گلایه‌های رایج است.

سلام یک فایل اجرایی دارد که می‌سازد، آزمون اجرا می‌کند، قالب‌بندی می‌کند، مستندات می‌سازد، سرور زبان اجرا می‌کند، کد را میان انگلیسی و فارسی ترجمه می‌کند و بسته‌های میزبانی‌شده در GitHub را مدیریت می‌کند. کتابخانه‌ی استانداردش برای زبانی به این جوانی به‌طور غیرمعمولی گسترده است (سرورهای HTTP و WebSocket، درایورهای پایگاه داده، TLS، فشرده‌سازی، تصویر، اجزای پایه‌ی یادگیری ماشین)، و این یعنی بسیاری از برنامه‌ها اصلاً به کد بیرونی نیاز ندارند. از سوی دیگر، اکوسیستم بسته‌های بیرونی بسیار کوچک است، جامعه کوچک است و زبان پیش از ۱٫۰ است، پس کدی که امروز نوشته می‌شود ممکن است پس از یک ارتقا به تغییر نیاز داشته باشد.

کی Rust و کی زبان سلام را انتخاب کنیم؟

Rust را انتخاب کنید وقتی:

  • ایمنی حافظه باید اثبات شود، نه آزموده: کد حساس امنیتی، تجزیه‌گرهای ورودی نامطمئن، زیرساخت‌هایی که مدت طولانی اجرا می‌شوند؛
  • به هم‌زمانی بدون رقابت داده نیاز دارید که کامپایلر تضمینش کند؛
  • به crates.io، اکوسیستم پخته‌ی async یا پشتیبانی سیستم‌های نهفته وابسته‌اید؛
  • به پایداری بلندمدت و بازار بزرگ نیروی کار نیاز دارید.

سلام را انتخاب کنید وقتی:

  • سرعت بومی و نبود زباله‌روب را همراه با زبانی کوچک‌تر، خواناتر و با منحنی یادگیری کوتاه می‌خواهید؛
  • کار، کد کاربردی است (ابزارها، سرویس‌ها، وب‌سرورها، پردازش داده) که در آن کتابخانه‌ی استاندارد گسترده در وقت صرفه‌جویی می‌کند؛
  • تیم شما مدیریت دستی حافظه را با پشتوانه‌ی آزمون‌ها، بررسی‌های ساخت توسعه و salam memcheck می‌پذیرد؛
  • می‌خواهید با همان کامپایلر و همان کتابخانه‌ها به فارسی برنامه بنویسید یا آموزش دهید.

اگر ایمنی حافظه یک الزام قطعی است، امروز پاسخ درست Rust است، و سلام ادعایی خلاف این ندارد.

پرسش‌های پرتکرار

آیا زبان سلام بررسی‌گر قرض (Borrow Checker) دارد؟

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

آیا سلام از Rust سریع‌تر است؟

در سنجش پروژه این دو در یک رده‌اند: روی ۳۹ برنامه‌ی کوتاه، میانگین سلام ۱۰٫۶ میلی‌ثانیه و میانگین Rust ۱۱٫۳ میلی‌ثانیه بود. Rust در برخی برنامه‌ها و سلام در برخی دیگر سریع‌تر بود، و فاصله‌ها آن‌قدر کوچک‌اند که نباید سنجش را رتبه‌بندی خواند.

آیا یادگیری زبان سلام از Rust آسان‌تر است؟

برای بیشتر افراد، بله. سلام طول عمر و قاعده‌های قرض ندارد و نحوش به پایتون و Go نزدیک‌تر است. بهای این سادگی آن است که کامپایلر اشتباه‌های حافظه‌ی کمتری را می‌گیرد.

آیا سلام و Rust می‌توانند با هم کار کنند؟

از راه ABI زبان C، بله. یک کتابخانه‌ی Rust که تابع‌های extern "C" صادر می‌کند از یک بلوک درون‌داد: در سلام قابل فراخوانی است، و کتابخانه‌ی سلامی که با salam build --lib ساخته شده تابع‌هایی به سبک C در اختیار می‌گذارد که Rust می‌تواند صدا بزند.

سلام و زبان‌های دیگر

مقایسه سلام و پایتون: سرعت کد بومی با نحوی ساده؟

سلام تقریباً مثل پایتون خوانده می‌شود، اما با گونه‌های ایستا و بدون زباله‌روب به کد ماشین کامپایل می‌شود. در این نوشته یک برنامه را در هر دو زبان می‌بینید، فاصله‌ی سرعتِ اندازه‌گیری‌شده را، و موارد زیادی را که پایتون هنوز انتخاب بهتری است.

خواندن مقاله
سلام و زبان‌های دیگر

مقایسه سلام و TypeScript: یک زبان برای مرورگر و سرور؟

TypeScript هر جا جاوااسکریپت باشد اجرا می‌شود و بزرگ‌ترین اکوسیستم نرم‌افزار را پشت سر دارد. سلام راه دیگری به «یک زبان برای کل پشته» می‌رود: سرور بومی، زبان صفحه و پشتوانه‌ی جاوااسکریپت. این نوشته تفاوت‌های عملی آن دو را نشان می‌دهد.

خواندن مقاله