اجرای کدهای پایتونی، ۱۰۰۰ بار سریع تر
بله درست متوجه شدید؛ راهی هست تا کدهای محاسباتی که به زبان پایتون نوشتیم را تا ۱۰۰۰ برابر سریع تر اجرا کنیم. در میان برنامه نویسان، زبان پایتون به زبانی سریع در توسعه و ساخت اما کند در اجرا در مقایسه با زبان هایی مثل C/C++ یا Java شناخته می شود. اما به کمک ابزاری که به شما معرفی می کنم، می توانید این محدودیت را کنار زده و توابع پایتونی را سریع تر اجرا کنید.
راز سرعت اجرای بالای زبان هایی مثل C در کامپایل آن ها به زبان ماشین نهفته است. ما نیز اگر بتوانیم کدهای نوشته شده به زبان پایتون را کامپایل کنیم، می توانیم سرعت اجرای کد خود را افزایش دهیم. البته این کامپایل کردن قدری با آن چه از زبان C سراغ داریم متفاوت است و ما یک فایل اجرایی تحویل نمی گیریم و از مفسر پایتون بی نیاز نمی شویم.
ابزارهای مختلفی برای افزایش سرعت اجرای کدهای پایتونی با کامپایل کردن آن ها وجود دارد که همه در دو دسته کامپایلرهای AOT (Ahead Of Time) و JIT (Just In Time) دسته بندی می شوند؛ موارد دسته اول کل کد پایتونی را دریافت کرده و یک فایل جداگانه ایجاد می کنند که باید آن فایل را با موتور پایتون اجرا کرد. اما دسته دوم قطعه هایی از کد را در لحظه اجرا کامپایل کرده و باعث افزایش سرعت می شوند. در جدول زیر مثال هایی از این ابزارها را مشاهده می کنید.
در این مقاله به ابزار Numba می پردازیم.
Numba چیست؟
Numba یک کامپایلر از نوع JIT است که به توسعه دهندگان اجازه می دهد بدون نوشتن قطعه کدهای C یا C++ اجرای کدهای پایتونی خود را سریع تر کنند. Numba یک پروژه متن باز است که در میان تحلیل گران داده و محاسبه گران علمی محبوب است و با پایتون ۲.۷ و ۳ سازگاری دارد.
Numba می تواند در حین اجرا توابع پایتونی را به کدهای ماشین بهینه شده ترجمه کند که برای این کار از پروژه LLVM بهره می برد. کد خروجی کامپایل شده توسط Numba می تواند به سرعت اجرای کدهای نوشته شده به زبان C یا حتی FORTRAN برسد. برای این کار نیازی نیست تا مفسر پایتونی خود را با چیز دیگری جایگزین کنید، مراحل مجزایی برای کامپایل طی کنید یا حتی یک کامپایلر C/C++ جداگانه نصب کنید. تنها کافی است یکی از دکوراتورهای Numba را به تابع پایتونی خود اعمال کرده و بقیه کار را به Numba بسپارید!
همانطور که از اسم Numba پیداست، به گونه ای طراحی شده تا روی کدهای حاوی آرایه و توابع NumPy استفاده شود. هم چنین Numba با Jupyter Notebooks سازگار است.
Numba چگونه کار می کند؟
در زمان اجرا Numba برای توابع یا متدهای پایتونی که با دکوراتور مشخص کردیم، کد ماشین ایجاد می کند. وقتی تابع مربوطه فراخوانی می شود، Numba بایت-کد پایتونی آن را به نمایه واسط LLVM (IR) کامپایل می کند. سپس با کمک LLVM نمایه واسط مربوطه را بهینه سازی کرده و از آن برای معماری CPU مورد استفاده، کد ماشین ایجاد می کند. البته این کتابخانه می تواند با کارت گرافیک نیز کار کند.
این فرآیند قدری زمان بر است؛ در نتیجه Numba از یک سیستم کشینگ بهره برده و کد ماشین ایجاد شده را ذخیره می کند تا در فراخوانی های بعدی آن تابع، دیگر آن را کامپایل نکند. در نتیجه در فراخوانی های بعدی سرعت اجرای تابع به طرز چشم گیری زیاد می شود.
همین جا متوجه می شویم Numba وقتی نقش کلیدی خود را ایفا می کند که بخواهیم یک تابع را چندین بار اجرا کنیم.
Numba چندین دکوراتور و تابع برای کنترل رفتار کامپایلر JIT در اختیار ما قرار می دهد. مثل دکوراتور jit@ که برای فعال سازی کامپایلر JIT روی تابع داده شده استفاده می شود و دکوراتور vectorize@ که تابع داده شده را به یک تابع برداری SIMD تبدیل می کند. (در واقع ufunc های نامپای می سازد.) برای دیدن تمام دکوراتورهای Numba به این لینک مراجعه کنید.
آیا Numba به کار من می آید؟
توجه کنید که ایده اصلی استفاده از Numba سریع تر کردن اجرای کدهای محاسباتی و ریاضیاتی زمانبر و سنگین است. ویژگی بارز این محاسبات این است که یک مجموعه عملکرد و تابع را بارها فراخوانی می کنند؛ در نتیجه با هر بار فراخوانی جدید، شاهد بهبود سرعت هستیم. اگر می خواهید توابع محاسباتی یا ریاضی را به کار بگیرید، استفاده از Numba پیشنهاد می شود.
Numba را چطور نصب کنیم؟
پکیج Numba در مخزن conda و توزیع پایتونی Anaconda موجود است که در صورت نصب بودن miniconda یا anaconda می توانید از خط فرمان با دستور زیر آن را روی محیط مجازی خود نصب کنید:
conda install numba
هم چنین در مخزن Pypi نیز برای آن یک wheel وجود دارد که می توانید با دستور زیر آن را نصب کنید:
pip install numba
در نهایت اگر کاربر حرفه ای هستید، می توانید Numba را از مرجع اصلی کامپایل کنید.
مثالی از کارکرد Numba
در این مثال تاثیر Numba و دکوراتور @jit آن را بر روی محاسبات تکراری زیر بررسی می کنیم. تابع add_numbers مجموع تمام اعداد صحیح کوچکتر از عدد دریافتی را بر می گرداند.
import numpy as np
from numba import jit
@jit
def add_numbers(n):
s = 0
for i in range(n):
s += i
return s
مشاهده می کنید که تنها با اعمال دکوراتور jit@ پیش از تعریف تابع، می توانیم تابعی که باید کامپایل شود را مشخص کنیم.
زمان اجرای کد بالا روی سیستم من برای اجرای اول معادل ۲۸۶ میلی ثانیه بود:
اما به محض اجرای دوباره تابع، زمان به ۱۱ میکروثانیه رسید! البته این طور نیست که پس از هر بار فراخوانی، زمان اجرای تابع کمتر و کمتر شود بلکه پس از اولین فراخوانی و کامپایل شدن، سرعت در یک محدود مشخصی قرار می گیرد.
مثال زیر که در مستندات Numba آمده هم جالب توجه است:
from numba import jit
import numpy as np
import time
x = np.arange(100).reshape(10, 10)
@jit(nopython=True)
def go_fast(a): # Function is compiled and runs in machine code
trace = 0.0
for i in range(a.shape[0]):
trace += np.tanh(a[i, i])
return a + trace
# DO NOT REPORT THIS... COMPILATION TIME IS INCLUDED IN THE EXECUTION TIME!
start = time.perf_counter()
go_fast(x)
end = time.perf_counter()
print("Elapsed (with compilation) = {}s".format((end - start)))
# NOW THE FUNCTION IS COMPILED, RE-TIME IT EXECUTING FROM CACHE
start = time.perf_counter()
go_fast(x)
end = time.perf_counter()
print("Elapsed (after compilation) = {}s".format((end – start)))
خروجی این کد به این صورت است:
Output:
Elapsed (with compilation) = 0.33030009269714355s
Elapsed (after compilation) = 6.67572021484375e-06s
محدودیت های Numba
Numba با همه خصوصیات دوست داشتنی اش، محدودیت هایی هم دارد:
- Numba توابع را کامپایل می کند و نه کل برنامه را.
- Numba همه بخش های زبان پایتون را پشتیبانی نمی کند؛ مثلا در حالی که با دیکشنری، لیست و set ها کار می کند اما اگر اجزای لیست یا زوج کلید و مقادیر دیکشنری ها، از انواع داده مختلفی با هم ترکیب شوند، برای Numba مشکل ساز می شود.
- Numba فقط برخی موارد زیر مجموعه NumPy را پشتیبانی می کند نه همه توابع و متدهای آن را.
- Numba پکیج های پایتونی دیگر مثل scikit-learn یا Pandas را پشتیبانی نمی کند. در این حالت کد بدون خطا و توسط مفسر پایتون اجرا شده و کامپایل نمی شود.
- Numba با آرایه های NumPy بهتر از List های پایتون کار می کند. (این مورد چندان محدودیتی به حساب نمی آید :) )
- با بستن خط فرمان یا محل اجرای کد، کش Numba هم حذف شده و برای اجراهای بعدی مجددا به کامپایل نیاز دارد.
نتیجه گیری
Numba یک ابزار قدرتمند برای بهبود عملکرد کدهای پایتونی شما است. به کمک Numba می توانید بدون استفاده از افزونه های نوشته شده به زبان C یا C++، اجرای محاسبات عددی و ریاضی و دیگر کارهای حساس محاسباتی را بسیار سریعتر کنید. البته اگر در حال تولید محصول تجاری هستید، نمی توانید مثل کامپایلرهای C یا C++ توقع ساخت فایل اجرایی با این کامپایلر JIT داشته باشید.
هر چند Numba انتخابی خوب و محبوبی برای محاسبات علمی و تحلیل داده است، در عین حال در کاربرد های مختلف گسترده ای که performance یک عنصر کلیدی به حساب می آید، قابل استفاده است. Numba با دکوراتورها و توابعی ساده، چه برای توسعه دهندگان تازه کار و چه توسعه دهندگان حرفه ای ابزاری به درد بخور به حساب می آید.
با همه این اوصاف می توان نتیجه گرفت با ارزش افزوده ای که Numba برای توسعه دهندگان پایتونی به ارمغان آورده، جای خود را در جعبه ابزار آن ها باز کرده تا بتوانند همه پتانسیل زبان پایتون را به عنوان زبانی محاسباتی با عملکردی قوی، آزاد کنند.
مرجع و منابع برای مطالعه
- https://numba.pydata.org/numba-doc/latest/user/5minguide.html
- https://www.analyticsvidhya.com/blog/2021/04/numba-for-data-science-make-your-py-code-run-1000x-faster/
- https://erogluegemen.medium.com/accelerating-your-python-code-with-numba-35ae63b22752
- https://numba.pydata.org/numba-doc/latest/user/performance-tips.html#performance-tips