GreatBahram
GreatBahram
خواندن ۷ دقیقه·۴ سال پیش

آنچه می‌بینید و آنچه نمی‌بینید!

Timing and Profiling Code
Timing and Profiling Code

مقدمه

سلام به همگی!

این هفته می‌خواهیم دو نوع ابزار خیلی خوب واسه اندازه‌گیری سرعت برنامه‌های پایتون معرفی کنیم.

فرض کنیم برای انجام یک کار، چند تا راه حل داریم و می‌خواهیم ببینیم کدوم‌اش‌ بهینه‌تر هست. این کار رو چطور انجام بدیم؟ معیارهایی که می خواهیم بررسی کنیم تجربی هستند. یعنی سراغ راه‌حل‌هایی مثلBig Notation و ... نمی‌ریم.

به عنوان مثال فرض کنید یه برنامه ساده داریم که دو ماتریس رو توی هم دیگه ضرب می‌کنه و سه تا راه حل این شکلی داریم:

حالا می‌خواهیم ببینیم کدوم یکی از این سه راه حل از سرعت بهتری برخوردار هست؟

روش اول

یک راه حل ساده این هست که ابتدای کار زمان سیستم رو چک کنیم و وقتی هم تست‌هامون به پایان رسید یک بار دیگه زمان رو چک کنیم و ببینم تفاضل این دوتا چقدر بوده، چیزی شبیه پایین:

How to time something in Python.
How to time something in Python.

اینجا ما با کمک ماژول time این کارو انجام دادیم. این ماژول چندین فانکشن واسه انجام این کار داره، ممکن هست شما قبلا از فانکشن time.time یا time.process_time استفاده کرده باشید. مشکل اولی این هست که روی همه‌ی پلتفرم‌ها از دقت یکسان برخودار نیست و دومی هم زمان sleep داخل برنامه رو حساب نمی‌کنه. بهمین خاطر کلا بهتر هست که از perf_counter استفاده کنید، کار کردن با این فانکشن‌ها خیلی ساده است؛ به تنهایی معنا ندارن و باید دوتا از اونا داشته باشین و تفاضل‌شون میشه زمانی که صرف کار شما شده.

اگر از طرفداری Jupyter Notebook هستید اونجا اتفاقا با راحتی بیشتری می‌تونید این کارو انجام بدید:

How to time something in the Jupyter Notebook
How to time something in the Jupyter Notebook

مزیت‌ این روش ساده بود‌ن‌اش هست و اما عیب‌اش چیه؟ عیب این روش اینه که کامپیوترها در هر لحظهِ وضعیت‌های متفاوتی دارن:

The downside of simple timing is its variance.
The downside of simple timing is its variance.

به همین خاطر شاید بهتر باشه که حداقل چندبار اجراش کنیم و نهایتاً میانگین خروجی‌ها رو معیار قرار بدیم.

اما به جای اینکه این کارو خودمون انجام بدیم، می‌ریم که با کمک روش دوم این مساله رو برطرف کنیم.

روش دوم

یک ماژول دیگه داخل پایتون هست به اسم timeit که رسالت خودش رو اینجوری معرفی می‌کنه:

Measure execution time of small code snippets

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

یه مدل از اجرای کامند‌لاینش رو با هم ببینیم:

با پارامتر n- مشخص می‌کنیم که قطعه کد داده شده رو چند بار اجرا کنه و خود کد رو به صورت یک رشته به عنوان آخرین ورودی بهش می‌دیم (این ماژول به کد ما میگه statement). توی حالت اول می‌بینیم که گفته حدودا ۱۰ مایکروثانیه زمان برده و وقتی مجموع ده برابری ‌اش رو حساب کردیم یکم بیشتر از ده برابر شده (این زمان حاصل تقسیم کل زمان اجرا بر تعداد اجرا‌ها هست).

حالا فرض کنید کارِ مشابه بالا رو این سری با NumPy انجام بدیم. یه جایی نیاز داریم که NumPy رو import کنیم. میشه داخل کدی که بهش میدیم بذاریم؛ اما نکته‌اش اینه که اونجا باید چیزهایی رو بذاریم که میخواید n بار تکرار بشه. چیزهایی که ثابت هستند و یکبار اجراشون برای n بار کفایت می‌کنه به عنوان setup بهش پاس می‌دیم:

The point is not how much the NumPy is faster than standard library or vice versa, is to learn using setup.
The point is not how much the NumPy is faster than standard library or vice versa, is to learn using setup.

همه کارهای بالا داخل مفسر پایتون هم شدنی هست:

زمانی که اینجا به ما برمیگردونه مجموع زمانی هست که برای اجرای کد بالا صرف شده. در حالی که توی نسخه command-line میانگین زمان رو به ما نشون می‌داد.

ولی کلا به نظر میرسه کار کردن باهاش یکم سخت هست چون اینجوری هر سری باید قطعه کد مورد نظرمون رو بیاریم تبدیل به یک رشته کنیم بعد بدیم به تایم‌ایت! به عبارت دیگه مشکل اینه که اقای تایم‌ایت به متغییرها و فانکشن‌های داخل مفسر دسترسی نداره.

Poor timeit module is blind just like love.
Poor timeit module is blind just like love.

اما این مساله به راحتی قابل برطرف شدن هست، کافیه namespace (or better to say symbol table) خودمون رو باهاش به اشتراک بذاریم اینجوری همه چیزهایی که ما داخل مفسر تعریف کردیم میبینه:

Let's cure the timeit's blindness.
Let's cure the timeit's blindness.

خیلی خب حالا که حسابی تبرمون رو تیز کردیم بریم یه مقایسه بین سه فانکشنی که نوشتیم داشته باشیم:

Comparison is the thief of joy.
Comparison is the thief of joy.

همون‌طور که انتظار داشتیم نامپای خیلی سریع‌تر از دو حالت بالاست. مشابه روش اول این کار رو میشه داخل Jupyter Notebook هم با راحتی بیشتری انجام داد ( یه نگاهی هم به timeit%% بندازید):

Using timeit module inside the Jupyter Notebook
Using timeit module inside the Jupyter Notebook

این ابزار جدید، timeit،‌ خیلی از جاها به کمک‌تون میاد و اگر دقت کنید معمولاً ادم‌ها توی Stackoverflow از اون برای این استفاده می‌کنند که مقایسه‌ای بین چند راه‌حل داشته باشن. مثلا اینجا پرسیده شده که چرا تاپل‌ها توی پایتون سریع‌تر از لیست‌ها هستند؟ یا اینجا می‌تونیم تفاوت list-comprehension در مقابل for-loop ببینیم.

موضوع دوم پروفایلینگ

ابزارهای بالا توی رده بنچمارکینگ می‌گنجند یعنی ما فقط یک دید کلی داریم که چقدر زمان برده تا کار مورد نظر ما انجام بشه. اما یه دسته دیگه از ابزارهای مفید، پروفایلر‌ها هستند. پروفایلر‌ها به ما کمک می‌کنند که کندترین بخش برنامه‌مون (bottleneck) رو پیدا کنیم و اگر راهی هست بریم اون رو برطرف کنیم.

پروفایلرهارو دو دسته کلی دارند:

  • اولی Determinticها: این دسته از پروفایلر‌ها در کل زمان اجرا برنامه‌ی شما رو رصد می‌کنند یعنی هر فانکشنی هر عملی که انجام بشه رو زیر نظر دارن و به همین دلیل overhead زیادی به برنامه تحمیل می‌کنند. خیلی خودمونی بگیم نسبت به حالت عادی برنامه کندتر اجرا میشه و معمولاً توی محیط تست و توسعه استفاده میشن. پروفایلی که خود پایتون به صورت پیش‌فرض داره، cProfile، جز این دسته است.
  • دومی Sampling/Statistical:‌ این روش به جای اینکه از ابتدا تا انتهای برنامه رفتار برنامه رو رصد کنه،‌ توی بازه‌های مشخصی نمونه گیری میکنه خب اینجوری سربار خیلی خیلی کمتر میشه اما دقت هم از اون طرف کمتر کاهش پیدا می‌کنه. داخل این دسته هم باز یه تنوع‌هایی داریم مثلا یکی حتما باید روی سیستم‌تون نصب بشه و کد رو باهاش اجرا کنید، Pyinstrument. یکی دیگه میگه external هست یعنی نیازی نیست که برنامه‌تون رو تغییر بدید فقط‌ بهش یک PID میدید و این شروع به جمع‌اوری اطلاعات می‌کنه،‌ مثل py-spy، این باعث میشه که py-spy یه گزینه‌ی خیلی خوبی برای محیط‌های پروداکشن باشه.

توی مثال پایین ما یک برنامه خیلی ساده داریم که ادای یک کار مثلا وقت گیر رو در میاره:

Our pretty simple example
Our pretty simple example

بعدش برای پروفایل کردنش به صورت پایین عمل کردیم. آپشن o- مشخص می‌کنه که خروجی پروفایلر کجا ذخیره بشه:

How to use python built-in profiler?
How to use python built-in profiler?

خروجی cProfile یک فایل باینری هست که با ابزارهای مختلفی میشه پارسش کرد. یک ابزار، داخل خود پایتون هست به اسم pstats که امکان سورت و چاپ خروجی داده رو میده، چیزی شبیه پایین:

Parse the profile output via pstats module.
Parse the profile output via pstats module.

خروجی بالا رو باید اینجوری خوندش:

روی هم رفته ۱۲ تا فانکشن توی این برنامه صدا زده شده که حدود ۵ ثانیه زمان اجرای همه اونا بوده. و گفته که براساس cumulative time مرتب شده که جلوتر میگم چی هست.

  • فیلد اول ncalls: تعداد فراخوانی‌ها رو نشون میده. به طور مثال، چون ما دو بار تابع پرینت رو اجرا کردیم میبیند دومی از اخر که فانکشن پرینت هست ncallsاش دو هست.
  • فیلدهای tottime: اینجا کل زمان صرف شده برای اجرای اون فانکشن بجز زمانی که صرف اجرای زیر مجموعه‌هاش شده. فیلد بعدی‌اش percalls میشه تقسیم tottime بر تعداد اجرهایی که داشته.
  • فیلد cumtime: مدت زمان اجرای فانکشن بعلاوه زیرمجموعه‌هاش رو نشون میده که مشابه قبلی اینجا هم percalls تقسیم اون زمان بر تعداد اجرای اون فانکشن رو به ما میگه.
  • در نهایت اسم فانکشن‌ای که اجرا شده و به همراه شماره خط اون رو می‌بینیم.

یک ابزار خوب دیگه هم که میشه با اون خروجی پروفایلر رو به صورت گرافیکی دید snakeviz هست.

برای مثال ما خروجی‌اش اینجوری میشه:

Or you could the famous Snakeviz module.
Or you could the famous Snakeviz module.

بعضی‌ها به پروفایلر پایتون ایراد می‌گیرند که وقتی کد خودم رو پروفایل می‌کنم دنبال اشکال داخل کد خودم می‌گردم چرا باید فراخوانی‌های داخلی پایتون رو هم توی خروجی ببینم؟ برخلاف cProfile دو ابزار Pyinstrument و scalene اینجوری کار نمی‌کنن و فقط خطوط برنامه‌ی خودتون رو نشون میدن. ابزار Scalene یه حسنی هم داره که درصد استفاده از CPU و Memory رو هم جداگانه نشون میده، اما توی cProfile ما فقط با wall-clock سروکار داریم.

داخل Jupyter notebook هم باز میشه تقریبا اکثر این کارها رو انجام و اینجا می‌تونید چند مثال از این حالت ببینید.

جمع بندی

قبل از اینکه این مطلب رو تموم کنم، دوست دارم به این نکته اشاره کنم که در حین توسعه دیوانه‌وار دنبال بهینه سازی نباشید. یه جمله‌ای هست که میگه:

Make it work, make it right, [and then] make it fast.

من خودم تمام تلاشم رو می‌کنم که تا جای ممکن از پیش بهینه‌سازی جلوگیری کنم. امیدوارم که از این مطلب چیزی یاد گرفته باشید و اگر هم دوست داشتید این مطلب رو با دوستاتون به اشتراک بذارید.

از اینجا کجا بریم؟

python
Pythonista, Free Software Enthusiast. GNU/Linux Master. Network Security Researcher. Son. Brother.
شاید از این پست‌ها خوشتان بیاید