سلام به همگی!
این هفته میخواهیم دو نوع ابزار خیلی خوب واسه اندازهگیری سرعت برنامههای پایتون معرفی کنیم.
فرض کنیم برای انجام یک کار، چند تا راه حل داریم و میخواهیم ببینیم کدوماش بهینهتر هست. این کار رو چطور انجام بدیم؟ معیارهایی که می خواهیم بررسی کنیم تجربی هستند. یعنی سراغ راهحلهایی مثلBig Notation و ... نمیریم.
به عنوان مثال فرض کنید یه برنامه ساده داریم که دو ماتریس رو توی هم دیگه ضرب میکنه و سه تا راه حل این شکلی داریم:
حالا میخواهیم ببینیم کدوم یکی از این سه راه حل از سرعت بهتری برخوردار هست؟
یک راه حل ساده این هست که ابتدای کار زمان سیستم رو چک کنیم و وقتی هم تستهامون به پایان رسید یک بار دیگه زمان رو چک کنیم و ببینم تفاضل این دوتا چقدر بوده، چیزی شبیه پایین:
اینجا ما با کمک ماژول time این کارو انجام دادیم. این ماژول چندین فانکشن واسه انجام این کار داره، ممکن هست شما قبلا از فانکشن time.time یا time.process_time استفاده کرده باشید. مشکل اولی این هست که روی همهی پلتفرمها از دقت یکسان برخودار نیست و دومی هم زمان sleep داخل برنامه رو حساب نمیکنه. بهمین خاطر کلا بهتر هست که از perf_counter استفاده کنید، کار کردن با این فانکشنها خیلی ساده است؛ به تنهایی معنا ندارن و باید دوتا از اونا داشته باشین و تفاضلشون میشه زمانی که صرف کار شما شده.
اگر از طرفداری Jupyter Notebook هستید اونجا اتفاقا با راحتی بیشتری میتونید این کارو انجام بدید:
مزیت این روش ساده بودناش هست و اما عیباش چیه؟ عیب این روش اینه که کامپیوترها در هر لحظهِ وضعیتهای متفاوتی دارن:
به همین خاطر شاید بهتر باشه که حداقل چندبار اجراش کنیم و نهایتاً میانگین خروجیها رو معیار قرار بدیم.
اما به جای اینکه این کارو خودمون انجام بدیم، میریم که با کمک روش دوم این مساله رو برطرف کنیم.
یک ماژول دیگه داخل پایتون هست به اسم timeit که رسالت خودش رو اینجوری معرفی میکنه:
Measure execution time of small code snippets
میبینید دیگه که میگه من ساخته شدم واسه اندازه گیری کدهای کوچیک. این ماژول خیلی باحاله، نه تنها داخل کدمون میتونیم ازش استفاده کنیم، بلکه امکان فراخونی داخل کامندلاین رو هم داره. کلا یه ابزار خیلی خیلی کاربردیه واسه اینجور مواقع است.
یه مدل از اجرای کامندلاینش رو با هم ببینیم:
با پارامتر n- مشخص میکنیم که قطعه کد داده شده رو چند بار اجرا کنه و خود کد رو به صورت یک رشته به عنوان آخرین ورودی بهش میدیم (این ماژول به کد ما میگه statement). توی حالت اول میبینیم که گفته حدودا ۱۰ مایکروثانیه زمان برده و وقتی مجموع ده برابری اش رو حساب کردیم یکم بیشتر از ده برابر شده (این زمان حاصل تقسیم کل زمان اجرا بر تعداد اجراها هست).
حالا فرض کنید کارِ مشابه بالا رو این سری با NumPy انجام بدیم. یه جایی نیاز داریم که NumPy رو import کنیم. میشه داخل کدی که بهش میدیم بذاریم؛ اما نکتهاش اینه که اونجا باید چیزهایی رو بذاریم که میخواید n بار تکرار بشه. چیزهایی که ثابت هستند و یکبار اجراشون برای n بار کفایت میکنه به عنوان setup بهش پاس میدیم:
همه کارهای بالا داخل مفسر پایتون هم شدنی هست:
زمانی که اینجا به ما برمیگردونه مجموع زمانی هست که برای اجرای کد بالا صرف شده. در حالی که توی نسخه command-line میانگین زمان رو به ما نشون میداد.
ولی کلا به نظر میرسه کار کردن باهاش یکم سخت هست چون اینجوری هر سری باید قطعه کد مورد نظرمون رو بیاریم تبدیل به یک رشته کنیم بعد بدیم به تایمایت! به عبارت دیگه مشکل اینه که اقای تایمایت به متغییرها و فانکشنهای داخل مفسر دسترسی نداره.
اما این مساله به راحتی قابل برطرف شدن هست، کافیه namespace (or better to say symbol table) خودمون رو باهاش به اشتراک بذاریم اینجوری همه چیزهایی که ما داخل مفسر تعریف کردیم میبینه:
خیلی خب حالا که حسابی تبرمون رو تیز کردیم بریم یه مقایسه بین سه فانکشنی که نوشتیم داشته باشیم:
همونطور که انتظار داشتیم نامپای خیلی سریعتر از دو حالت بالاست. مشابه روش اول این کار رو میشه داخل Jupyter Notebook هم با راحتی بیشتری انجام داد ( یه نگاهی هم به timeit%% بندازید):
این ابزار جدید، timeit، خیلی از جاها به کمکتون میاد و اگر دقت کنید معمولاً ادمها توی Stackoverflow از اون برای این استفاده میکنند که مقایسهای بین چند راهحل داشته باشن. مثلا اینجا پرسیده شده که چرا تاپلها توی پایتون سریعتر از لیستها هستند؟ یا اینجا میتونیم تفاوت list-comprehension در مقابل for-loop ببینیم.
ابزارهای بالا توی رده بنچمارکینگ میگنجند یعنی ما فقط یک دید کلی داریم که چقدر زمان برده تا کار مورد نظر ما انجام بشه. اما یه دسته دیگه از ابزارهای مفید، پروفایلرها هستند. پروفایلرها به ما کمک میکنند که کندترین بخش برنامهمون (bottleneck) رو پیدا کنیم و اگر راهی هست بریم اون رو برطرف کنیم.
پروفایلرهارو دو دسته کلی دارند:
توی مثال پایین ما یک برنامه خیلی ساده داریم که ادای یک کار مثلا وقت گیر رو در میاره:
بعدش برای پروفایل کردنش به صورت پایین عمل کردیم. آپشن o- مشخص میکنه که خروجی پروفایلر کجا ذخیره بشه:
خروجی cProfile یک فایل باینری هست که با ابزارهای مختلفی میشه پارسش کرد. یک ابزار، داخل خود پایتون هست به اسم pstats که امکان سورت و چاپ خروجی داده رو میده، چیزی شبیه پایین:
خروجی بالا رو باید اینجوری خوندش:
روی هم رفته ۱۲ تا فانکشن توی این برنامه صدا زده شده که حدود ۵ ثانیه زمان اجرای همه اونا بوده. و گفته که براساس cumulative time مرتب شده که جلوتر میگم چی هست.
یک ابزار خوب دیگه هم که میشه با اون خروجی پروفایلر رو به صورت گرافیکی دید snakeviz هست.
برای مثال ما خروجیاش اینجوری میشه:
بعضیها به پروفایلر پایتون ایراد میگیرند که وقتی کد خودم رو پروفایل میکنم دنبال اشکال داخل کد خودم میگردم چرا باید فراخوانیهای داخلی پایتون رو هم توی خروجی ببینم؟ برخلاف cProfile دو ابزار Pyinstrument و scalene اینجوری کار نمیکنن و فقط خطوط برنامهی خودتون رو نشون میدن. ابزار Scalene یه حسنی هم داره که درصد استفاده از CPU و Memory رو هم جداگانه نشون میده، اما توی cProfile ما فقط با wall-clock سروکار داریم.
داخل Jupyter notebook هم باز میشه تقریبا اکثر این کارها رو انجام و اینجا میتونید چند مثال از این حالت ببینید.
قبل از اینکه این مطلب رو تموم کنم، دوست دارم به این نکته اشاره کنم که در حین توسعه دیوانهوار دنبال بهینه سازی نباشید. یه جملهای هست که میگه:
Make it work, make it right, [and then] make it fast.
من خودم تمام تلاشم رو میکنم که تا جای ممکن از پیش بهینهسازی جلوگیری کنم. امیدوارم که از این مطلب چیزی یاد گرفته باشید و اگر هم دوست داشتید این مطلب رو با دوستاتون به اشتراک بذارید.