آزادسازی عملکرد با DataFrameهای غیرقابل تغییر در پایتون بدون GIL

 

استفاده از اجرای موازی در پردازش سطرهای DataFrame با Python 3.13 آزاد از GIL

اعمال یک تابع بر روی هر سطر یک DataFrame، یکی از عملیات‌های رایج در پردازش داده‌ها است. این عملیات‌ها ذاتاً «قابل موازی‌سازی» هستند؛ چرا که هر سطر به طور مستقل قابل پردازش است. اگر از پردازنده‌ای با چند هسته استفاده کنیم، می‌توان چندین سطر را به‌صورت هم‌زمان پردازش کرد.

اما تا همین اواخر، بهره‌گیری از این فرصت در زبان Python ممکن نبود. اجرای توابع با چند ترد (multi-threaded) به دلیل CPU-bound بودن، با مانع بزرگی به نام Global Interpreter Lock (GIL) مواجه بود و عملکرد موازی واقعی غیرممکن بود.

نسخه آزمایشی Python 3.13 بدون GIL

اکنون Python راه‌حلی ارائه کرده است: نسخه آزمایشی free-threading در Python 3.13، GIL را حذف کرده و امکان اجرای هم‌زمان واقعی برای عملیات‌های وابسته به پردازنده را فراهم کرده است.

نتایج عملکرد شگفت‌انگیز هستند. با استفاده از Python آزاد از GIL، نسخه 3.2 از کتابخانه StaticFrame می‌تواند اجرای توابع سطری را دست‌کم دو برابر سریع‌تر از حالت تک‌ترد انجام دهد.

مثالی از عملکرد

برای مثال، در یک DataFrame مربعی با یک میلیون عدد صحیح، می‌خواهیم مجموع اعداد زوج در هر سطر را محاسبه کنیم:

lambda s: s.loc[s % 2 == 0].sum()

در نسخه آزاد از GIL (Python 3.13t)، زمان اجرا از 21.3 میلی‌ثانیه به 7.89 میلی‌ثانیه کاهش می‌یابد (بیش از 60٪ کاهش):

>>> import numpy as np; import static_frame as sf

>>> f = sf.Frame(np.arange(1_000_000).reshape(1000, 1000))
>>> func = lambda s: s.loc[s % 2 == 0].sum()

>>> %timeit f.iter_series(axis=1).apply(func)
21.3 ms ± 77.1 μs

>>> %timeit f.iter_series(axis=1).apply_pool(func, use_threads=True, max_workers=4)
7.89 ms ± 60.1 μs

در StaticFrame، برای اعمال توابع سطری، از iter_series(axis=1) همراه با apply() (برای اجرا تک‌ترد) یا apply_pool() (برای اجرا موازی با use_threads=True) استفاده می‌شود.


تفاوت عملکرد در Python استاندارد

در نسخه استاندارد پایتون با GIL فعال، اجرای چندتردی نه تنها مزیت ندارد، بلکه می‌تواند باعث کاهش عملکرد شود:

>>> %timeit f.iter_series(axis=1).apply(func)
17.7 ms ± 144 µs

>>> %timeit f.iter_series(axis=1).apply_pool(func, use_threads=True, max_workers=4)
39.9 ms ± 354 µs

نکات مهم در استفاده از Python بدون GIL

البته باید توجه داشت که نسخه آزاد از GIL با مقداری سربار همراه است. مثلاً در مثال بالا، اجرای تک‌ترد در Python 3.13t کندتر از نسخه استاندارد بود (21.3 در مقابل 17.7 میلی‌ثانیه).

این موضوع بخشی از توسعه فعال CPython است و بهبودهای بیشتر در نسخه 3.14t و بعد از آن انتظار می‌رود.


چرا StaticFrame مناسب اجرای موازی است؟

کتابخانه StaticFrame با اعمال ویژگی تغییرناپذیری (immutability) بر روی داده‌ها، باعث می‌شود که به‌طور ذاتی امن در برابر تردها باشد؛ بنابراین نیازی به استفاده از قفل‌ها یا نسخه‌های کپی داده‌ها نیست.

StaticFrame از آرایه‌های NumPy تغییرناپذیر استفاده می‌کند و هرگونه تغییر درجا (in-place mutation) را ممنوع می‌کند.


آزمایش‌های گسترده عملکرد روی DataFrame

برای ارزیابی جامع عملکرد، آزمایش‌هایی روی ۹ نوع مختلف DataFrame با ترکیب‌هایی از اشکال (بلند، مربع، عریض) و ساختارهای داده (یکنواخت، ترکیبی، ستونی) انجام شده است.

در همه تست‌ها، تابع مورد استفاده همان تابع ساده برای جمع مقادیر زوج است.

در شکل‌های مختلف، از گزینه‌های use_threads=True (برای ترد) و use_threads=False (برای فرآیند جداگانه) استفاده شده است. همچنین از max_workers برای تعیین تعداد تردها یا پردازش‌ها بهره گرفته شده.


یافته‌ها:

  • در Python 3.13t، کاهش زمان اجرا در همه انواع DataFrame مشاهده شد؛ بین 50٪ تا حتی بیش از 80٪.
  • برای داده‌های بزرگ (مثلاً 100 میلیون مقدار)، عملکرد بسیار بهبود یافته و اجرای هم‌زمان مقرون‌به‌صرفه است.
  • در MacOS و Linux هر دو، مزایای مشابهی مشاهده شده، با اندکی برتری برای MacOS.
  • حتی روی DataFrame کوچک (10,000 مقدار)، در ساختارهای tall و square، اجرای موازی در 3.13t سودمند بوده است.

قبل از free-threaded Python چه داشتیم؟

در نسخه‌های قبلی، تنها راه برای استفاده از پردازنده‌های چند هسته‌ای، multi-processing بود. اما این روش هم هزینه زیادی داشت و فقط در کارهای بسیار سنگین مقرون‌به‌صرفه بود.

در این مقاله نشان داده شده که multi-processing روی عملیات‌های کوچک (مانند اجرای سطری تابع)، زمان اجرا را به طرز قابل توجهی بدتر می‌کند.


وضعیت فعلی Python بدون GIL

  • PEP 703 در جولای ۲۰۲۳ پذیرفته شد: GIL اختیاری شود.
  • PEP 779 در ژوئن ۲۰۲۵ تأیید شد: Python 3.14 free-threaded به‌طور رسمی پشتیبانی خواهد شد.
  • در فاز سوم، این قابلیت احتمالاً به‌طور پیش‌فرض فعال می‌شود، اما هنوز زمان آن مشخص نیست.

جمع‌بندی

اعمال توابع بر روی سطرهای DataFrame فقط شروع راه است. بسیاری از عملیات‌های دیگر مثل group-by، اعمال sliding window، و تحلیل‌های پیچیده‌تر نیز می‌توانند از اجرای موازی بهره‌مند شوند.

نسخه‌های جدید Python نه تنها سریع‌تر شده‌اند (مثلاً Python 3.14 حدود 20 تا 40٪ سریع‌تر از Python 3.10 است)، بلکه با free-threaded Python می‌توانند حتی در بارهای کاری مبتنی بر C-extension مثل NumPy نیز عملکرد بالاتری ارائه دهند.

اکنون که امکان اشتراک‌گذاری داده‌های تغییرناپذیر میان تردها وجود دارد، فرصت‌های زیادی برای بهینه‌سازی واقعی در دسترس توسعه‌دهندگان پایتون قرار گرفته است.

فهرست مطالب

پیمایش به بالا