اول بیاید یک مقایسه کوچک انجام بدیم:
در تصویر بالا جمع اعداد حسابی از صفر تا یک میلیارد رو :
آیا پایتون کند است؟ خیر به شرطی که از ابزار درستی استفاده کنید!
در کل وقتی کد پایتون داره execute میشه، اتفاقی که میوفته اینه: بایتکدهای تولید شده از کد شما درون یک حلقه و switch statement خیلی بزرگی در فایل ceval.c قرار میگیره، هربار یک بایتکد وارد حلقه میشه و کد مخصوص اون بایتکد در switch statement پیدا میشه و ران میشه.
ران شدنش هم راحته، صرفا چند تا تابع به زبان سی قرار اجرا بشه (میتونید توی داکیومنتیشن C API پیداشون کنید.)
سوال اینجاست که حالا که کدهای سی اجرا میشن چرا در مورد اول (pure_summer) کد، کُند هست اما sum_summer خیلی سریعتر هست؟
در واقع بیشترِ زمانیکه در مورد اول تلف شده بخاطر وجود همون حلقه و switch statement بزرگ در ceval.c هست. تابع pure_summer در کل ۱۵ بایتکد تولید میکنه که ۷ تاش مال تابع for و عامل کندکننده کد ماست! هر بار iteration در ceval.c برای ما هزینه داره و وقت رو تلف میکنه و روی هم رفته باعث کند شدن تابع اول میشه. (۷ تا بایتکدِ مربوط به حلقه قراره چندبار اجرا بشن و تبعا قراره چند بار iteration در ceval.c اتفاق بیوفته؟؟)
اما در مورد دوم (sum_summer) ما ۶ تا بایتکد تولید شده داریم:
پایتون این ۶ تا رو اجرا میکنه و ما نتیجه رو داریم، فرقش با بالایی چیه؟
۱. تابع sum یک تابع built-in هست و یک حلقه به زبان C داره که کاملا مشخصه که از حلقه پایتون سریع تره
۲. پایتون دیگه قرار نیست پشت سر هم به دفعات زیاد iteration داشته باشه. صرفا در یک iteration تابع رو صدا میزنه و اون تابع درون خودش یه حلقه زبان سی داره و وقتی کارش تموم شد نتیجه رو به پایتون میده. این دو فاکتور باعث میشن که مورد دوم بسیار سریعتر از مورد اول باشه
Python with built-in `sum` vs. Pure Python ≃ 3.75 times faster
مورد سوم که دارای یک دکوریتور به اسم numba.jit@ هست، قضیهاش کمی با باقی توابع فرق داره، وقتی که میایم به بایتکد تولیدی این تابع نگاه میکنیم میبینیم که هیچ تفاوتی با تابع اولی نداره، اما قرار نیست این بایتکدها اجرا بشن.
وقتی این تابع رو دکوریت میکنیم چنین آبجکتی رو بجای تابع معمولی استفاده میکنیم:
این چیزی هست که اون دکوریتور jit برای ما تولید میکنه و همین قراره اجرا بشه، اما JIT چی هست اصلا؟
کامپایلرهای JIT، کامپایلرهایی هستن که وقتی که کد داره اجرا میشه، قسمتهایی از کد که دارای حلقههای سنگین و محاسبات بالا (اغلب عددی) هستن، و یا نقاطی که زیاد اجرا میشن، مثلا یک تابع که دفعات زیادی اجرا میشه، یا جاهایی که به اصطلاح نقاط داغ کد ما هستن رو پیدا میکنن و اونارو به چیز دیگهای، یک کد native و بسیار optimize شده، کامپایل میکنن. زبانها و رانتایمهایی که از JIT استفاده میکنن: یک. جاوا دو. سیشارپ سه. NodeJS و در دنیای پایتون: یک. مفسر PyPy، دو. مفسر Pyjion و سه. چیزی به اسم numba که مفسر نیست و صرفا یک JIT Compiler هست که در CPython میتونیم استفاده کنیم.
در مورد سوم هم دقیقا این اتفاق افتاده، ما تابع رو با دکوریتور jit، دکوریت کردیم و نامبا اون رو با یک کد خیلی خیلی سریع جایگزین کرده و وقتی که اجراش میکنیم، پایتون صرفا اون کد رو به سیپییو و سیپییو نتیجه رو به پایتون میده و پایتون به ما برش میگردونه.
جیت کامپایلر numba توسط کسی نوشته شده که NumPy رو نوشته، و مزایا و محدودیتهای خودش رو هم داره، این JIT Compiler برای سریعکردن قسمتهایی از کد استفاده میشه که محاسبات عددی و چنین حلقههایی که در مقایسهمون دیدیم، داشته باشن؛ پس همهجا نمیشه از این JIT Compiler استفاده کرد، اما اگه در جای درستش استفاده کنید پیشرفت:
Python + numba vs. Pure Python ≃ 5,880,000 times faster
Python + numba vs. Python built-in `sum` ≃ 1,746,666 times faster
«خیلی» بزرگی رو خواهید دید.
جیت compilerها هم مزایا و هم معایبی دارن. از مزایاشون میشه به همین افزایش نجومی سرعت بخشهایی از کد ما اشاره کرد. از معایبشون:
۱. وقتی از JIT Compiler استفاده میکنید، قراره بی شک مموری بیشتری استفاده کنید، بنچمارکهای مختلفی وجود داره، مثلا در یکی که الگوریتم n-body رو در چند زبان برنامهنویسی مقایسه کرده بود، جولیا در این بنچمارک ۴.۲۱ ثانیه، NodeJS در 8.55 ثانیه، پایتون (CPython) در 9 دقیقه اون رو تمام کرده بودن.
اما میزان استفاده از مموری رو اگه نگاه کنیم: جولیا عدد ۲۲۶۴۲۰ نود جاس ۳۹۹۵۶ و پایتون ۷۷۸۰ (واحدش رو نمیدونم) رو داشتن که:
نود جیاس بیش از ۵ برابر و جولیا بیش از ۲۹ برابر مصرف مموری بیشتری داشتن.
۲. جیت کامپایلرها اندکی start-up رو کند میکنن، چون jit compiler ها برنامههای پیچیدهای هستن و تا بیان به خودشون بجنبن و کاملا ران بشن، طول میکشه. این رو میشه در همون عکس از بنچمارک ببینید، در اولین اجرای jit_summer ما یک زمان رو مشاهده میکنیم و در دومین یک زمان دیگه؛ زمان اصلیای که باید در نظر بگیریم اون دومی هست. چرا؟ چون در بار اول نامبا باید تابع رو دریافت کنه، تصمیم بگیره و سپس یک کد خیلی سریع براش تولید کنه و بعد پایتون اون رو اجرا کنه؛ اما نامبا این کد رو cache میکنه و بار دوم فقط اون کد اجرا میشه و ما زمان واقعی اجرا شدن اون تابع رو میبینیم.
دلیل اصلیای که پایتون در این بنچمارک کند ظاهر شده دقیقا همون دلیلی هست که برای مورد اول گفتیم. این الگوریتم خیلی پیچیدهای نیست اما محاسبات و tight loopهای سنگین و زیادی داره که باعث میشه وقت تلف شده زیادی رو در پایتون شاهد باشیم.
۳ ویدیو خیلی خوب که بهتره ببینیدشون
همهمون میدونیم که هر چیزی رو بهر کاری ساختن، و نمیشه از یه چیزی همهجا استفاده کرد؛ این جمله برای نامبا هم دقیقا صدق میکنه. بیاید تا چند تا مثال رو ببینیم:
در این مثال ساده یک تابع داریم که یک ورودی لیست داره که برای هر عضو چک میکنه که آیا زوج هست یا نه، اگه زوج بود که عدد دو رو به یک لیست دیگه append میکنه، اگه نه استرینگ ۱ رو. و بعدش اون لیست رو به ما میده.
ابتدا با پایتون اجراش میکنیم، یک میلی ثانیه و تقریبا ۴ دهم میلی ثانیه طول میکشه تا اجرا بشه.
حالا بیاید این تابع رو جیت کامپایل و اجرا کنیم:
با یک warning طویل و قرمز رو به رو میشیم :(
یکی از مهمترین چیزایی که توی این warning میبینیم همین خطی هست که نوشته
object mode compilation
اگه با نامبا کار میکنید و این اخطار رو دریافت کردید بدونید که هیچ افزایش سرعتی عاید شما نمیشه.
خب برای اولین بار که قبلا هم صحبت کردیم وقت سر کامپایل شدن اون تابع میگذره بیاید تا دوباره اجراش کنیم:
میبینیم که از نسخه pure Python اش خیلی کندتر هست.
این رفتار نامبا چند دلیل داره. اگه بریم و اون اخطار رو بخونیم متوجه میشیم اولین دلیلش اینه که ما یبار به لیست عدد وارد کردیم و یبار string. خب این رفتار اشتباهی هست چون نامبا میخواد یک کد خیلی efficient عه low level برای محاسبه عددی تولید کنه، میبینه عه یه استرینگ داره به لیست اضافه میشه و همهچیز بهم میریزه. دلیل دومش هم اینکه نامبا با لیستهای پایتون رابطه خوبی نداره یک دلیلش اینه که هر آبجکتی میشه بهشون اضافه کرد که دقیقا اشکال کار ما در دلیل اول همین بود، دو اینکه یک سایز فیکس نداره و این برای کارها و بهینهسازیهای مربوط به مموری کار رو خراب میکنه.
بخاطر این دو دلیل نامبا یک کد به کندی کد پایتون ما تولید میکنه، و بخاطر overheadعی هم که داره در کل نتیجه کندتر میشه.
برای اینکه اندکی بهتر بشه اوضاع این مثالمون، ما اینکار رو انجام میدیم:
اول از همه اون یک رو به عدد تبدیل میکنیم و سپس از یک دکوریتور دیگه از نامبا به اسم njit استفاده میکنیم، این دکوریتور دقیقا همون دکوریتور jit قبلی هست اما یک آرگومان رو برای ما جلو جلو پاس داده و اون آرگومان:
nopython=True
هست. کاری که این آرگومان انجام میده اینکه نامبا دیگه از آبجکتهای پایتون و C API عه پایتون استفاده نمیکنه و در اکثر مواقع توصیه میشه از همین دکوریتور استفاده کنید. اما وقتی این رو ران میکنیم به یک اخطار دیگه برخورد میکنیم و این دفعه مشکل زیر سر لیست ورودی ماست.
ارور میگه من از حالتی به اسم reflected list استفاده میکنم. این حالت بخاطر همون مشکل نامبا با لیستهای پایتون هست و اگه برای بار دوم این رو ران کنیم:
باز هم یه نتیجه خیلی بد میگیریم.
راه حل چیه؟
باید از برادر بزرگتر نامبا به اسم نامپای NumPy استفاده کنیم :))
بجای یک لیست معمولی از تابع arange عه نامپای استفاده میکنیم، وقتی تابع اولیه مون رو ران میکنیم میبینیم که نسخه پایتون کندتر از قبل ظاهر میشه (دلیلش رو من دقیقا نمیدونم که بهتون بگم) اما وقتی اون رو جیت کامپایل و ران میمیکنیم یک افزایش سرعت زیادی رو میبینیم.
Pure Python: 1.71 ms
Python + Numba: 282 micro s
Pure Python vs. Python + Numba => about 6 times faster
کار با نامبا بعضی وقتا حساسیتهای زیادی داره و همینجوری نمیشه همهچیز رو سریعتر کرد، باید داکیومنتیشناش رو بخونید، ویدیوهای متفاوت و کدهای دیگه رو ببینید تا بتونید به خوبی ازش استفاده کنید.
سیستمی که ازش برای تست گرفتن استفاده کردم:
OS: Debian GNU/Linux 11 (bullseye) x86_64
Kernel: 5.10.0-12-amd64CPU: AMD Ryzen 7 3700U with Radeon Vega Mobile Gfx (8) @ 2.300
Python: Python 3.9.2 (default, Feb 28 2021, 17:03:44)
[GCC 10.2.1 20210110] on linux
نامبا پایتون را سریع میکند.
نامبا یک JIT Compiler متنبازه که قسمتی از کد پایتون و NumPy شما را به یک machine code بسیار سریع تبدیل میکند.
نامبا توابع پایتون را در زمان اجرا به کد ماشین بهینهسازی شده با استفاده از کتابخانه استاندارد کامپایلر LLVM ترجمه می کند. الگوریتم های عددی کامپایل شده با Numba در پایتون می توانند به سرعت های C یا FORTRAN نزدیک شوند.
نیازی نیست مفسر پایتون را جایگزین کنید، یک مرحله کامپایل جداگانه اجرا کنید، یا حتی یک کامپایلر C/C++ را نصب کنید. کافیست یکی از دکوریتورهای Numba را روی تابع پایتون خود اعمال کنید و Numba بقیه کارها را انجام می دهد.
نامبا برای استفاده با آرایه ها و توابع NumPy طراحی شده است. Numba کد تخصصی را برای انواع داده ها و طرح بندی های مختلف آرایه تولید می کند تا عملکرد را بهینه کند. دکوراتورهای ویژه می توانند universal functions ایجاد کنند که مانند توابع NumPy روی آرایه های NumPy پخش می شوند.
Numba همچنین با نوتبوکهای Jupyter برای محاسبات تعاملی و با چارچوبهای اجرایی توزیعشده، مانند Dask و Spark، عالی کار میکند.
نامبا طیف وسیعی از گزینه ها را برای موازی سازی کد شما برای CPU و GPU ارائه می دهد، که اغلب تنها با تغییرات جزئی کد همراه است.
نامبا می تواند به طور خودکار expressionهای آرایههای NumPy را روی چندین هسته CPU اجرا کند و نوشتن حلقه های موازی را آسان می کند.
نامبا می تواند به طور خودکار برخی از حلقهها را به vector instructions برای بهبود سرعت تا 2-4 برابر تبدیل کند. Numba با قابلیت های CPU شما سازگار است، چه CPU شما از SSE، AVX یا AVX-512 پشتیبانی کند.
نامبا با پشتیبانی از درایورهای NVIDIA CUDA و AMD ROCm به شما امکان میدهد الگوریتمهای GPU موازی را کاملاً با پایتون بنویسید.
وبسایت نامبا:
اطلاعات بیشتر در مورد نامبا:
کنفرانسها و آموزشها:
گیتهاب:
سریع برنامهنویسی کنید :))