مقدمه

در درس‌های گذشته دیدیم که استفاده از تکرارگرها و کلوژرها منجر به کدی می‌شود که بسیار گویاتر، مختصرتر و نزدیک‌تر به سبک برنامه‌نویسی تابعی است. اما یک سوال طبیعی که برای بسیاری از برنامه‌نویسان، به خصوص آنهایی که از زبان‌های سیستمی می‌آیند، پیش می‌آید این است: آیا این انتزاع سطح بالا (high-level abstraction) هزینه عملکردی دارد؟ به عبارت دیگر، آیا یک حلقه for دستی که خودمان می‌نویسیم، سریع‌تر از یک زنجیره از متدهای تکرارگر مانند iter().map().filter() نیست؟

در بسیاری از زبان‌های برنامه‌نویسی، پاسخ به این سوال "بله" است. اما Rust بر اساس یک اصل کلیدی به نام «انتزاع‌های بدون هزینه» (Zero-Cost Abstractions) طراحی شده است.

اصل انتزاع بدون هزینه

این اصل به این معنی است که شما نباید برای استفاده از قابلیت‌های سطح بالاتری که Rust ارائه می‌دهد، هزینه عملکردی در زمان اجرا بپردازید. کامپایلر Rust بسیار هوشمند است و طوری طراحی شده که این انتزاع‌های سطح بالا را در زمان کامپایل به کدهای ماشین بسیار بهینه‌ای ترجمه کند که اغلب به همان سرعت (و گاهی حتی سریع‌تر از) کدهای سطح پایینی است که شما به صورت دستی می‌نویسید.

در مورد تکرارگرها، کامپایلر Rust یک بهینه‌سازی بسیار مهم به نام «ادغام حلقه» یا loop fusion را انجام می‌دهد. وقتی شما یک زنجیره از آداپتورهای تکرارگر را می‌نویسید، کامپایلر تمام آن فراخوانی‌های مجزا را در هم ادغام کرده و یک حلقه واحد و بهینه تولید می‌کند، بدون اینکه هیچ هزینه سربار (overhead) اضافی برای فراخوانی‌های متعدد ایجاد شود.

یک بنچمارک ساده

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

Copy Icon src/lib.rs
// Implementation using a manual for loop
pub fn sum_of_squares_loop(input: &Vec<i32>) -> i32 {
    let mut sum = 0;
    for &num in input {
        if num % 2 == 0 {
            sum += num * num;
        }
    }
    sum
}

// Implementation using iterators
pub fn sum_of_squares_iterator(input: &Vec<i32>) -> i32 {
    input
        .iter()
        .filter(|&&num| num % 2 == 0)
        .map(|&num| num * num)
        .sum()
}

نسخه تکرارگر به وضوح خواناتر و گویاتر است. اما آیا کندتر است؟ اگر شما این دو تابع را با ابزارهای بنچمارکینگ Rust (مانند cargo bench) و با فعال بودن بهینه‌سازی‌های release اجرا کنید، خواهید دید که عملکرد آنها تقریباً یکسان است. کامپایلر Rust زنجیره iter().filter().map().sum() را به یک کد ماشین بسیار بهینه تبدیل می‌کند که معادل حلقه دستی ماست.

نتیجه‌گیری: چه زمانی از کدام استفاده کنیم؟

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

  • خوانایی: سبک تابعی و زنجیره‌ای تکرارگرها، هدف کد را بسیار واضح‌تر بیان می‌کند.
  • قابلیت نگهداری: افزودن یک مرحله جدید به پردازش (مثلاً یک filter دیگر) در زنجیره تکرارگرها بسیار ساده‌تر از ویرایش یک حلقه for تودرتو است.
  • عملکرد: شما بدون هیچ هزینه عملکردی، از مزایای خوانایی و نگهداری بهتر بهره‌مند می‌شوید. کامپایلر کار بهینه‌سازی را برای شما انجام می‌دهد.

تنها در موارد بسیار نادر و خاص که نیاز به کنترل سطح پایین و بسیار دقیق روی حلقه دارید، ممکن است یک حلقه دستی گزینه بهتری باشد. اما برای ۹۹٪ موارد، تکرارگرها راه حل اصولی و کارآمد در Rust هستند.

در این درس، با بررسی اصل انتزاع بدون هزینه، دیدیم که چرا نباید نگران عملکرد تکرارگرها در مقایسه با حلقه‌های سنتی باشیم. با این درس، فصل «ویژگی‌های Functional در Rust» به پایان می‌رسد. ما در این فصل با قدرت کلوژرها و تکرارگرها برای نوشتن کدهای گویاتر و کارآمدتر آشنا شدیم. در فصل بعدی، به بررسی قابلیت‌های پیشرفته‌تر ابزار Cargo خواهیم پرداخت و یاد می‌گیریم که چگونه پروژه‌های خود را برای انتشار آماده کرده و با اکوسیستم بزرگ Rust تعامل کنیم.