پرش به محتوای اصلی
پرش به محتوای مقاله

کارایی Rust در برابر محدودیت‌های اشیاء پایتون در PyO3

·۲۲ شهریور ۱۴۰۵۵ دقیقه مطالعه۱ بازدید
راهنما
اجرای کد Rust در Python با PyO3: رویکرد کتابخانه‌های معروف
اجرای کد Rust در Python با PyO3: رویکرد کتابخانه‌های معروف
اشتراک‌گذاری
واقعاً چه چیز جدید است؟

شناسایی دقیق هزینه تبدیل داده (Materialization Cost) به عنوان گلوگاه اصلی در جایگزینی پایتون با Rust؛ جایی که هزینه تبدیل اشیاء می‌تواند از هزینه اجرای خود الگوریتم بیشتر شود.

اگر امروز برای افزایش سرعت پردازش داده‌ها به Rust پناه می‌برید، احتمالاً متوجه شده‌اید که سریع‌ترین کدها هم در لحظه بازگشت به پایتون کند می‌شوند. نبرد واقعی برای سرعت نه در قلب الگوریتم، بلکه در مرز میان این دو زبان رخ می‌دهد. یک تجزیه‌گر JSON با کارایی بالا که به زبان Rust نوشته شده باشد، می‌تواند تا ۳.۵ برابر سریع‌تر از نسخه بومی پایتون اجرا شود، اما چالش اصلی در نقطه‌ای است که این دو زبان با هم تلاقی می‌کنند.

به گزارش belderbos.dev در ۱۳ سپتامبر ۲۰۲۶، ابزار PyO3 به توسعه‌دهندگان اجازه می‌دهد Rust را درون پایتون جاسازی کنند؛ تکنیکی که همین حالا هسته Pydantic v2 را از طریق کتابخانه pydantic-core قدرت می‌بخشد. برای اکثر برنامه‌نویسان پایتون، انعطاف‌پذیری این زبان با یک «مالیات عملکرد» همراه است. وقتی یک کتابخانه باید حجم عظیمی از داده‌ها را اعتبارسنجی یا تجزیه کند، توسعه‌دهندگان برای انجام کارهای سنگین به Rust روی می‌آورند. این کار یک معماری ترکیبی می‌سازد که در آن Rust محاسبات را مدیریت می‌کند و پایتون رابط کاربری سطح بالا (High-level API) را بر عهده دارد.

همان‌طور که در تحلیل‌های پیشین ما درباره بهینه‌سازی زیرساخت‌های داده اشاره کردیم، ادغام این دو زبان نیازمند یک خط لوله چهارمرحله‌ای است:
۱. نوشتن یک ماژول معمولی در Rust.
۲. افزودن متادیتای PyO3 به کد.
۳. کامپایل و نصب ماژول توسط ابزار Maturin.
۴. وارد کردن (Import) نتیجه در محیط پایتون.

توسعه‌دهندگان از ماکروهای PyO3 — به‌ویژه #[pyfunction] و #[pymodule] — برای ایجاد کدهای رابط (Glue Code) استفاده می‌کنند. این ماکروهای ویژگی (Attribute Macros) شبیه به دکوراتورهای پایتون عمل کرده و تابع را به‌گونه‌ای بازنویسی می‌کنند که تبدیل انواع داده و شمارش ارجاعات (Reference Counting) در مرز دو زبان مدیریت شود. سپس Maturin کد Rust را به یک کتابخانه مشترک (مانند .so در لینوکس، .dylib در مک یا .dll در ویندوز) تبدیل کرده و آن را در محیط مجازی (Virtual Environment) قرار می‌دهد تا دستور import به‌سادگی کار کند.

تجزیه‌گر در سمت Rust

قبل از اینکه داده‌ها به پایتون برسند، تجزیه‌گر یک enum ساده در Rust تولید می‌کند. در یک دوره آموزشی تخصصی برای انتقال پایتون به Rust، دانشجویان شش هفته زمان صرف ساخت یک توکن‌ساز (Tokenizer) و تجزیه‌گر بازگشتی (Recursive-descent Parser) از صفر کردند، بدون اینکه از کتابخانه معروف serde استفاده کنند.

طبق گزارش‌های این دوره، یکی از دانشجویان به نام جاش نسخه‌ای ساخت که در تست‌های واقعی روی داده‌های نمونه، از ماژول json در CPython (که با زبان C نوشته شده) سریع‌تر بود. دانشجوی دیگری به نام یوخن توانست به سرعتی ۳.۵ برابر سریع‌تر از نسخه پایتون دست یابد. هسته این سیستم یک JsonValue است که درخت JSON را به‌دقت نقشه‌برداری می‌کند:

  • Null
  • Boolean(bool)
  • Number(f64)
  • String(String)
  • Array(Vec<JsonValue>)
  • Object(HashMap<String, JsonValue>)

این درخت کاملاً در محیط Rust زندگی می‌کند و پایتون هرگز خودِ این enum را نمی‌بیند، بلکه فقط نتیجه تبدیل‌شده و سازگار با پایتون را دریافت می‌کند.

سازوکار تبدیل داده‌ها

برای معرفی یک تابع به پایتون، تنها دو خط کد کافی است. امضای تابع از py: Python<'py> استفاده می‌کند که توکنی برای دسترسی به مفسر پایتون است. در ساختارهای سنتی پایتون، این دسترسی با نگه داشتن قفل جهانی مفسر (GIL) مرتبط است.

برای بازگرداندن داده‌ها، تابع از PyResult<Bound<'py, PyAny>> استفاده می‌کند. در اینجا Bound<'py, PyAny> در واقع دسته‌ای برای یک شیء پایتون از هر نوعی است (معادل PyObject در زبان C) و PyResult پوششی برای Result<T, PyErr> است. اگر تابع parse در Rust با شکست مواجه شود، اپراتور ? خطا را به بالا منتقل می‌کند و پایتون یک Exception استاندارد را مشاهده می‌کند.

برای ترجمه مقادیر Rust، توسعه‌دهندگان ویژگی (Trait) IntoPyObject را پیاده‌سازی می‌کنند. این Trait تعیین می‌کند یک مقدار Rust چگونه به شیء پایتون تبدیل شود:

  • Null: به py.None().into_bound(py) تبدیل می‌شود.
  • Number: مقدار f64 به یک float پایتون تبدیل می‌شود.
  • Object: یک HashMap باعث ایجاد یک PyDict می‌شود که سپس به‌صورت بازگشتی برای هر جفت کلید-مقدار، متد into_pyobject را فراخوانی می‌کند.
  • Array: یک Vec به یک لیست (List) پایتون تبدیل می‌شود.

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

بر اساس بررسی‌های belderbos.dev، حیاتی‌ترین نکته عملکردی این است که مسیر بازگشت از Rust به پایتون اغلب گران‌ترین بخش فرآیند است. در حالی که تجزیه یک رشته JSON در Rust فوق‌العاده سریع است، اما تبدیل (Materialization) آن به اشیاء پایتون رایگان نیست.

اگر یک سند شامل ۱۰۰,۰۰۰ مقدار باشد، فراخوانی into_pyobject باید ۱۰۰,۰۰۰ شیء مجزای پایتون ایجاد کند. در اسناد بزرگ، این حلقه تبدیل می‌تواند زمان اجرای نهایی را به تسلط درآورد و حتی از خودِ فرآیند تجزیه (Parsing) زمان‌برتر باشد.

مدیریت خطاها در دو زبان

ابزار PyO3 ترجمه خطاها را به‌گونه‌ای مدیریت می‌کند که لایه Rust در تجربه کاربر پایتون «نشت» نکند. با پیاده‌سازی ویژگی From، خطاهای خاص Rust به استثناهای استاندارد پایتون تبدیل می‌شوند.

برای مثال، گونه‌ی JsonError::UnterminatedString به یک PyValueError تبدیل می‌شود تا برنامه‌نویس پایتون یک خطای آشنا را همراه با موقعیت دقیق (Offset) شکست در تجزیه ببیند. به همین ترتیب، خطاهای std::io::Error به‌طور خودکار به استثناهای متناظر پایتون تبدیل می‌شوند؛ به این معنی که اگر مسیر یک فایل اشتباه باشد، پایتون یک FileNotFoundError استاندارد را برمی‌گرداند.

پیامدهای معماری

برای کسانی که کد خود را به Rust منتقل می‌کنند، استراتژی به نوع داده بازگشتی بستگی دارد. اگر تابع یک مقدار تک (Scalar) مانند یک عدد یا رشته برگرداند، هزینه مرزی ناچیز است. اما برای ساختارهای بزرگ، تبدیل داده‌ها گلوگاه اصلی است.

برای بهینه‌سازی این وضعیت، توسعه‌دهندگان می‌توانند گام‌های زیر را بردارند:

  • پیش‌تخصیص (Preallocation): تخصیص پیش‌فرض فضای PyDict می‌تواند در حاشیه به بهبود سرعت کمک کند.
  • بارگذاری تنبل (Lazy Loading): اجتناب از تبدیل کل درخت به اشیاء پایتون، در صورتی که فراخواننده به تمام بخش‌های آن نیاز ندارد.
  • نماهای مبتنی بر Rust (Rust-Backed Views): بازگرداندن یک نمای تنبل و ساخت اشیاء پایتون فقط در زمان درخواست (On-demand).

این تغییر دیدگاه — از تحلیل الگوریتم به تحلیل مرز تبدیل — تعیین می‌کند که آیا انتقال به Rust واقعاً در محیط عملیاتی سودمند است یا خیر. سریع کردن Rust بخش آسان ماجراست؛ مدیریت سفر مقادیر از Rust به اشیاء پایتون است که ارزش این انتقال را تعیین می‌کند.

گام بعدی شما

  • اگر از کتابخانه‌هایی مثل Pydantic استفاده می‌کنید، بررسی کنید که آیا حجم داده‌های خروجی شما باعث کندی در لایه تبدیل شده است یا خیر.
  • در پروژه‌های Rust-Python، به‌جای بازگرداندن ساختارهای پیچیده، سعی کنید از Lazy Views برای کاهش تعداد اشیاء ایجاد شده در پایتون استفاده کنید.
  • مستندات PyO3 را برای پیاده‌سازی بهینه IntoPyObject مطالعه کنید تا از ایجاد اشیاء تکراری جلوگیری شود.

اما داستان سخت‌افزاری این تحول حتی شگفت‌انگیزتر است — به تحلیل ما درباره تراشه‌های Blackwell مراجعه کنید.

چرا این موضوع مهم است؟

این یافته بر اساس تجربه عملی در توسعه کتابخانه‌های سطح بالا مانند Pydantic است و نشان می‌دهد که افزایش سرعت خام در Rust لزوماً به معنای سرعت بیشتر در پایتون نیست. درک هزینه Materialization اشیاء برای مهندسان نرم‌افزار ضروری است تا از انتقال‌های بی‌ثمر به Rust اجتناب کنند.

تأثیر برای ایران

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

·نگاه ما
تحریریه دات‌هوش

تمرکز توسعه‌دهندگان از بهینه‌سازی پیچیدگی زمانی الگوریتم‌ها به بهینه‌سازی «هزینه انتقال داده» تغییر کرده است. این نشان می‌دهد که در سیستم‌های ترکیبی، گلوگاه دیگر CPU نیست، بلکه مدیریت حافظه و تبدیل انواع داده (Type Conversion) در مرز دو زبان است. استراتژی‌های Lazy Loading در این لایه، جایگزین بهینه‌سازی‌های سنتی در کد Rust می‌شوند.

منابع

این گزارش با خط‌لولهٔ خودکار دات‌هوش از منابع معتبر جهانی تدوین و زیر نظر تحریریه منتشر شده است. روش کار ما

گفتگو

پنج‌شنبه‌های هوش‌محور

بسته‌ی هفتگی دات‌هوش

۵ خبر، ۲ ابزار، ۱ پرامپت در هر شماره. به‌زودی راه‌اندازی می‌شود — هر پنج‌شنبه صبح.

خبر کلیدی
ابزار کاربردی
پرامپت حرفه‌ای
تحلیل پژوهش
به‌زودی
زاویه‌ی ایرانی
به‌زودی
تمرین این هفته
به‌زودی

راهنماهای دات‌هوش

راهنماهای کاربردیِ دات‌هوش برای کار با هوش مصنوعی — از همین‌جا شروع کنید:

دات‌هوش

راهنمای فارسی هوش مصنوعی — با نگاه به ایران

اخبار روزانه، معرفی ابزارها و مدل‌ها، و آموزشِ کار با هوش مصنوعی؛ همیشه با این پرسش که از ایران چه چیزی کار می‌کند و چه چیزی نه.