پایتون سریعتر با PyPy
# PyPy چیست؟
پایتون یکی از محبوب ترین زبان های برنامه نویسی دنیا است. توسعه دهندگان عاشق پایتون هستند اما مثل هر چیز دیگری، پایتون بی نقص نیست و محدودیتهایی دارد. بسته به نوع برنامهای که توسعه میدهید ممکن است سرعت پایتون به نسبت زبانهای سطح پایین تا صد برابر نیز کمتر باشد. بخاطر همین است مسئله سرعت است که گاهی شرکتها مجبور میشوند برنامه هایشان را به زبان های دیگر بازنویسی کنند. اما چه میشد اگر میتوانستیم ویژگی های خوب پایتون را نگه داشته و سرعت آنرا افزایش دهیم؟ اینجاست که PyPy وارد عمل میشود.
PyPy یک مفسر بسیار سازگار پایتون است که جایگزین مناسبی برای CPython است. با نصب و اجرای برنامه خود با آن، می توانید پیشرفت های قابل توجهی در سرعت ببینید. میزان افزایش سرعت بستگی به نوع برنامهای دارد که مینویسید.
در این مقاله یاد میگیرید که:
- چطور PyPy را نصب کرده و برنامه هایتان را با آن اجرا کنید
- چطور PyPy از لحاظ سرعت با CPython مقایسه میشود
- ویژگیهای PyPy چیست و چطور کدهایتان را سریعتر اجرا میکند
- محدودیتهای PyPy چیست
مثالهای این مقاله با پایتون +3.6 سازگار هستند.
دوره پیشنهادی: دوره آموزش پایتون (python)
# پایتون و PyPy
پایتون به شکلهای مختلفی توسعه داده میشود تا برای نیازهای مختلفی پاسخگو باشد. مثلا CPython به زبان C و Jython به زبان java و IronPython به net. ساخته شده. باید بدانید که PyPy به زبان پایتون نوشته شده است.
CPython پیاده سازی اصلی پایتون بوده و با اختلاف محبوبترین پیاده سازی است. وقتی مردم میگویند پایتون، به احتمال زیاد دارند به CPython اشاره میکنند. شما هم به احتمال زیاد دارید از CPython استفاده میکنید.
اما از آنجایی که CPython یک زبان سطح بالا است از نظر سرعت آنچنان تعریفی ندارد. اینجاست که PyPy به کار میآید. از آنجایی که PyPy به مشخصات زبان پایتون پایبند است، نیازی به تغییر در کد شما ندارد و به لطف ویژگی هایی که در زیر می بینید، می تواند سرعت را به شکل قابل توجهی را بهبود بخشد.
حالا ممکن است برایتان سوال باشد که اگر سینتکس CPython و PyPy یکی است، پس چرا CPython از ویژگیهای خوب PyPy استفاده نمیکند تا سرعتش را بیشتر کند. بخاطر اینست که استفاده از آن ویژگیها، نیازمند تغییر کد گسترده در CPython است.
مقاله پیشنهادی: درک فانکشن main در پایتون
+ نصب PyPy
سیستم عامل شما ممکن است همین الآن هم PyPy را داشته باشد. مثلا، در macOS میتوانید با دستور زیر، PyPy را نصب کنید:
$ brew install pypy3
اگر ندارید، میتوانید فایلهای باینری PyPy را دانلود کرده و با دستورات زیر آن را نصب کنید:
$ tar xf pypy3.6-v7.3.1-osx64.tar.bz2
$ ./pypy3.6-v7.3.1-osx64/bin/pypy3
Python 3.6.9 (?, Jul 19 2020, 21:37:06)
[PyPy 7.3.1 with GCC 4.2.1]
Type "help", "copyright", "credits" or "license" for more information.
+ PyPy در عمل
حالا که PyPy را نصب کردهاید میتوانید از آن استفاده کنید. برای اینکار یک فایل به نام script.py را ساخته و کد زیر را در آن بنویسید:
total = 0
for i in range(1, 10000):
for j in range(1, 10000):
total += i + j
print(f"The result is {total}")
در این اسکریپت، دو حلقه for تودرتو داریم که اعداد 1 تا 9999 را اضافه کرده و نتیجه را چاپ میکند. برای اینکه ببینیم این کد چقدر زمان میبرد تا اجرا شود، کد را به شکل زیر تغییر دهید:
import time
start_time = time.time()
total = 0
for i in range(1, 10000):
for j in range(1, 10000):
total += i + j
print(f"The result is {total}")
end_time = time.time()
print(f"It took {end_time-start_time:.2f} seconds to compute")
کد به شکل زیر کار میکند:
- در خط 3، زمان حال حاضر را در متغیر start_time ذخیره میکنیم.
- در خطوط 5 تا 8، حلقهها را اجرا میکنیم.
- در خط 10، نتیجه را چاپ میکنیم.
- در خط 12، زمان حال حاضر را در متغیر end_time ذخیره میکنیم.
- در خط 13، تفاوت بین start_time و end_time را محاسبه کرده و مشخص میکنیم که کد چقدر طول کشیده تا اجرا شود.
بیایید کد را ابتدا با پایتون اجرا کنیم:
$ python3.6 script.py
The result is 999800010000
It took 20.66 seconds to compute
حالا با PyPy اجرا کنیم:
$ pypy3 script.py
The result is 999800010000
It took 0.22 seconds to compute
در کدی به این سادگی، PyPy تقریبا 94 برابر سرعت بیشتری نسبت به CPython داشته.
برای تستهای جدیتر، میتوانید به speed center مراجعه کنید که در اینجا توسعه دهندگان حرفهای انواع مختلفی از تستها را انجام دادهاند.
به یاد داشته باشید که میزان تاثیر سرعت بستگی به این دارد که چه کدی مینویسید. در بعضی موارد PyPy حتی کندتر نیز هستند. اما به طور معمول PyPy تقریبا 4.3 برابر سریعتر از CPython است.
# ویژگیهای PyPy
به طور کلی PyPy به دو مورد اشاره دارد:
- یک چارچوب زبان پویا برای تولید مفسر برای زبانهای پویا
- پیاده سازی پایتون با استفاده از آن چارچوب
شما مورد دوم را دیدید و با آن یک اسکریپت کوچک اجرا کردید. پیاده سازی پایتونی که استفاده کردید با استفاده از یک چارچوب زبان پویا به نام RPython نوشته شده است، درست مانند CPython به زبان C و Jython در جاوا.
اما آیا ما به شما نگفتیم که PyPy به زبان پایتون نوشته شده؟ خب، یک مقدار پیچیدهتر از اینهاست. دلیل اینکه PyPy به عنوان یک مفسر Python نوشته شده در Python (و نه در RPython) شناخته شد این است که RPython از همان سینتکس Python استفاده می کند.
برای اینکه همه چیز روشن شود، PyPy به این شکل ایجاد میشود:
1. سورس کد به زبان RPython نوشته شده است.
2. زنجیره ابزار مفسر RPython روی کد اعمال می شود که اساسا کد را کارآمدتر می کند. همچنین کد را به کد ماشین کامپایل می کند، به همین دلیل است که کاربران مک، ویندوز و لینوکس باید نسخه های مختلف را دانلود کنند.
3. یک فایل اجرایی باینری تولید می شود. این مفسر پایتون است که برای اجرای اسکریپت کوچک خود از آن استفاده کردید.
به یاد داشته باشید که نیازی نیست این مراحل را طی کنید تا بتوانید از PyPy استفاده کنید. برنامه اجرایی همین الآن هم موجود است و میتوانید آنرا دانلود و استفاده کنید.
همچنین، از آنجایی که استفاده از یک کلمه هم برای چارچوب و هم برای پیاده سازی بسیار گیج کننده است، تیم توسعه دهنده PyPy تصمیم گرفتند از این استفاده مضاعف دور شود. حالا PyPy فقط به به پیاده سازی پایتون اشاره میکند. زنجیره ابزار مفسر RPython به عنوان چارچوب در نظر گرفته میشود.
در قسمت بعد در رابطه با ویژگیهایی صحبت میکنیم که باعث میشود PyPy سریعتر و بهتر شود.
+ کامپایلر JIT
قبل از پرداختن به این که کامپایل JIT چیست، بیایید یک قدم به عقب برگردیم و ویژگی های زبان های کامپایل شده مانند C و زبان های تفسیر شده مانند جاوا اسکریپت را مرور کنیم.
زبان های برنامه نویسی کامپایل شده کارایی بیشتری دارند اما به سختی به معماری های CPU و سیستم عامل های مختلف منتقل می شوند. زبان های برنامه نویسی تفسیر شده قابل حمل تر هستند، اما عملکرد آنها بسیار بدتر از زبان های کامپایل شده است.
سپس زبان های برنامه نویسی مانند پایتون هستند که ترکیبی از کامپایل و تفسیر را انجام می دهند. به طور خاص، پایتون ابتدا به یک بایت کد میانی کامپایل می شود که سپس توسط CPython تفسیر می شود. این باعث می شود کد پایتون، بهتر از کد نوشته شده در یک زبان برنامه نویسی تفسیر شده عمل کند و مزیت قابل حمل را نیز حفظ می کند.
با این حال، عملکرد پایتون هنوز نزدیک به نسخه کامپایل شده نیست. دلیل آن این است که کد کامپایل شده می تواند بهینه سازی های زیادی را انجام دهد که با بایت کد امکان پذیر نیست.
اینجاست که کامپایلر (JIT) وارد میشود. JIT سعی میکند با انجام برخی کامپایلهای واقعی در کد ماشین و برخی در تفسیر، قسمتهای بهتر هر دو جهان را به دست آورد. به طور خلاصه، در اینجا مراحل کامپایل JIT برای ارائه عملکرد سریعتر آمده است:
- شناسایی پرکاربردترین ویژگیهای کد مانند یک فانکشن در حلقه
- تبدیل آن بخشها به کد ماشین در زمان اجرا
- بهینه کردن آن کد ماشین
- تعویض اجرای قبلی با نسخه کد ماشین بهینه شده
دو حلقه تو در تو در ابتدای آموزش را به خاطر دارید؟ PyPy تشخیص داد که همان عملیات بارها و بارها اجرا می شود، آن را در کد ماشین کامپایل کرد، کد ماشین را بهینه کرد و سپس پیاده سازی ها را تعویض کرد. به همین دلیل است که شاهد چنین پیشرفت بزرگی در سرعت بودید.
+ Garbage Collection
هر زمان که متغیر، فانکشن یا هر آبجکت دیگری را ایجاد می کنید، رایانه شما به آنها بخشی از حافظه را اختصاص می دهد. در نهایت، برخی از آن آبجکتها دیگر مورد نیاز نخواهند بود. اگر آنها را پاک نکنید، ممکن است حافظه رایانه شما تمام شود و برنامه شما از کار بیفتد.
در زبان های برنامه نویسی مانند C و C++ معمولاً باید به صورت دستی با این مشکل مقابله کنید. سایر زبان های برنامه نویسی مانند پایتون و جاوا این کار را به صورت خودکار برای شما انجام می دهند. به این کار automatic garbage collection گفته می شود و تکنیک های مختلفی برای انجام آن وجود دارد.
CPython از تکنیکی به نام reference count استفاده می کند. در این روش پایتون تعداد اشارات به یک آبجکت را بررسی میکند، زمانی که تعداد اشارات به صفر برسد، پایتون به طور اتوماتیک، حافظه اختصاص داده شده به آن آبجکت را خالی میکند. این یک تکنیک ساده و موثر است، اما یک نکته وجود دارد. مشکل اینجاست که خالی کردن حافظه باعث توقف پایتون شده و در زمان خالی کردن کاری انجام نمیدهد.
یا بعضی اوقات هست که reference count کلا کار نمیکند. مثلا کد زیر را در نظر بگیرید:
class A(object):
pass
a = A()
a.some_property = a
del a
در کد بالا یک کلاس جدید ایجاد کردید. سپس، از آن کلاس یک نمونه جدید ایجاد کرده و یک property به آن اضافه کردید. و در آخر آن نمونه را حذف کردید.
در این حالت، نمونه a دیگر در دسترس نیست. اما، reference count آن را از حافظه حذف نمیکند چون هنوز به خودش یک اشاره دارد. به این مشکل reference cycle گفته میشود و با reference count حل نمیشود.
در اینجاست که CPython از ابزار دیگری به نام cyclic garbage collector استفاده میکند. این ابزار در کل حافظه میچرخد و آبجکتهایی که استفادهای ندارند را حذف میکند. اما اگر تعداد آبجکتهای زیادی در حافظه باشد، دوباره باعث میشود که پایتون زمانی را متوقف شود.
اما، PyPy دیگر از reference count استفاده نمیکند. و فقط از روش دوم، cycle finder استفاده میکند. و بجای انجام پاکسازی در یک عملیات بزرگ، اینکار را تکه تکه انجام میدهد.
# محدودیتهای PyPy
PyPy یک گلوله نقره ای نیست و ممکن است همیشه مناسب ترین ابزار برای کار شما نباشد. حتی ممکن است برنامه شما را بسیار کندتر از CPython کند. به همین دلیل مهم است که محدودیت های زیر را در نظر داشته باشید.
+ با برنامههای C به خوبی کار نمیکند
PyPy با برنامه های پایتون خالص بهترین کار را دارد. هر زمان که از ماژول افزونه C استفاده می کنید، بسیار کندتر از CPython اجرا می شود. دلیل آن این است که PyPy نمی تواند ماژول های C را بهینه سازی کند، زیرا آنها به طور کامل پشتیبانی نمی شوند.
+ فقط با برنامههای بلند مدت خوب کار میکند
هنگامی که یک اسکریپت را با PyPy اجرا می کنید، کارهای زیادی برای اجرای سریعتر کد شما انجام می دهد. اگر اسکریپت خیلی کوچک باشد، کار سنگینی که انجام میدهد، باعث می شود اسکریپت شما کندتر از CPython اجرا شود. از سوی دیگر، اگر یک اسکریپت طولانی مدت دارید، می تواند سود قابل توجهی در عملکرد داشته باشد.
# نتیجه گیری
PyPy یک جایگزین سریع و توانا برای CPython است. با اجرای اسکریپت خود با آن، می توانید بدون ایجاد یک تغییر در کد خود، سرعت زیادی را بهبود ببخشید. اما محدودیت هایی دارد و باید برنامه خود را تست کنید تا ببینید آیا PyPy می تواند کمک کننده باشد یا خیر.