مقدمه
پایتون ممکن است یکی از محبوبترین زبانهای برنامهنویسی امروزی باشد، اما قطعاً کارآمدترین نیست. بهویژه در دنیای یادگیری ماشین، کاربران کارایی را فدای سادگی استفادهای میکنند که پایتون ارائه میدهد.
اما این بدان معنا نیست که نتوانید از روشهای دیگر سرعت را افزایش دهید. سایثون روشی آسان برای کاهش زمان محاسباتی اسکریپتهای پایتون است، بدون اینکه کارکردی که با پایتون به راحتی بهدست میآید، فدای آن شود.
این آموزش شما را با استفاده از سایثون برای افزایش سرعت اسکریپتهای پایتون آشنا خواهد کرد. ما به یک وظیفه ساده اما پرهزینه پردازشی میپردازیم: ایجاد یک حلقه for که از طریق یک لیست پایتون با 1 میلیارد عدد پیمایش کرده و آنها را جمع میکند. از آنجایی که زمان در هنگام اجرای کد روی دستگاههای با منابع محدود اهمیت ویژهای دارد، این مسئله را با در نظر گرفتن نحوه پیادهسازی کد پایتون در سایثون روی Raspberry Pi (RPi) بررسی خواهیم کرد. سایثون تغییرات قابلتوجهی در سرعت محاسبات ایجاد میکند؛ مثل یک تنبل در مقابل یک یوز.
پیشنیازها برای بهینهسازی اسکریپتهای پایتون با سایثون
- دانش پایهای از پایتون: آشنایی با سینتکس، توابع، انواع داده و ماژولهای پایتون.
- درک مفاهیم پایهای C/C++: آشنایی با مفاهیم پایهای C یا C++ مانند اشارهگرها، انواع داده و ساختارهای کنترلی.
- محیط توسعه پایتون: نصب پایتون (ترجیحاً پایتون 3.x) با یک مدیر بسته مانند pip.
- نصب سایثون: نصب سایثون با دستور pip install cython.
- آشنایی با ترمینال/خط فرمان: توانایی پایه برای جابجایی و اجرای دستورات در ترمینال یا خط فرمان.
این پیشنیازها به شما کمک میکنند تا برای شروع بهینهسازی کد پایتون با استفاده از سایثون آماده شوید.
پایتون و سیپایتون
بسیاری از افراد از این حقیقت بیخبر هستند که زبانهایی مانند پایتون در واقع در زبانهای دیگر پیادهسازی شدهاند. به عنوان مثال، پیادهسازی پایتون با زبان C به نام سیپایتون (CPython) شناخته میشود. توجه داشته باشید که این با سایثون (Cython) فرق دارد. برای اطلاعات بیشتر در مورد پیادهسازیهای مختلف پایتون، میتوانید این مقاله را مطالعه کنید.
پیادهسازی پیشفرض و محبوبترین پایتون، سیپایتون است. استفاده از آن یک مزیت مهم دارد. زبان C یک زبان کامپایل شده است و کد آن به کد ماشین تبدیل میشود که مستقیماً توسط واحد پردازش مرکزی (CPU) اجرا میشود. حالا شاید بپرسید، اگر C یک زبان کامپایل شده است، آیا این به این معناست که پایتون هم همینطور است؟
پیادهسازی پایتون در C (سیپایتون) 100% کامپایل شده نیست و همچنین 100% تفسیر شده هم نیست. در واقع، هم کامپایل و هم تفسیر در فرآیند اجرای یک اسکریپت پایتون وجود دارد. برای روشن شدن این موضوع، مراحل اجرای یک اسکریپت پایتون را بررسی میکنیم:
- کامپایل کردن کد منبع با استفاده از سیپایتون برای تولید بایتکد
- تفسیر بایتکد توسط مفسر سیپایتون
- اجرای خروجی مفسر سیپایتون در ماشین مجازی سیپایتون
فرآیند کامپایل زمانی اتفاق میافتد که سیپایتون کد منبع (.py فایل) را کامپایل کرده و بایتکد سیپایتون (.pyc فایل) را تولید میکند. سپس بایتکد سیپایتون توسط مفسر سیپایتون تفسیر شده و خروجی در ماشین مجازی سیپایتون اجرا میشود. طبق مراحل بالا، فرآیند اجرای یک اسکریپت پایتون شامل هم کامپایل و هم تفسیر است.
کامپایلر سیپایتون تنها یکبار بایتکد را تولید میکند، اما مفسر هر بار که کد اجرا میشود فراخوانی میشود. معمولاً تفسیر بایتکد زمان زیادی میبرد. اگر استفاده از مفسر باعث کاهش سرعت اجرا میشود، چرا اصلاً از آن استفاده کنیم؟ دلیل اصلی این است که مفسر باعث میشود پایتون برای سیستمعاملهای مختلف قابل استفاده باشد. از آنجایی که بایتکد در ماشین مجازی سیپایتون که بر روی CPU اجرا میشود، مستقل از ماشین اجرا میشود، میتوان آن را بدون تغییر در ماشینهای مختلف اجرا کرد.
اگر از مفسر استفاده نشود، کامپایلر سیپایتون کد ماشین را تولید خواهد کرد که مستقیماً در CPU اجرا میشود. زیرا پلتفرمهای مختلف دستورالعملهای متفاوتی دارند، بنابراین کد نمیتواند بر روی پلتفرمهای مختلف اجرا شود.
خلاصه اینکه، استفاده از کامپایلر فرآیند را سریعتر میکند، اما مفسر کد را چندپلتفرمی میسازد. بنابراین، یکی از دلایلی که Python کندتر از C است، استفاده از مفسر است. به یاد داشته باشید که کامپایلر تنها یک بار اجرا میشود، اما مفسر هر بار که کد اجرا میشود، اجرا خواهد شد.
Python خیلی کندتر از C است، اما بسیاری از برنامهنویسان هنوز آن را ترجیح میدهند زیرا استفاده از آن بسیار راحتتر است. Python بسیاری از جزئیات را از برنامهنویس پنهان میکند که میتواند از اشکالزدایی ناامیدکننده جلوگیری کند. به عنوان مثال، چون Python یک زبان نوعدینامیک است، نیازی به مشخص کردن نوع هر متغیر در کد نیست – Python بهطور خودکار آن را استنباط میکند. در مقابل، در زبانهای نوعاستاتیک (مانند C، C++ یا Java) شما باید انواع متغیرها را مشخص کنید، همانطور که در زیر نشان داده شده است.
int x = 10
string s = "Hello"
مقایسه با پیادهسازی زیر در Python:
نوعدینامیک باعث میشود کدنویسی آسانتر باشد، اما بار بیشتری بر روی ماشین وارد میکند تا نوع داده مناسب را پیدا کند. این موضوع فرآیند اجرا را کندتر میکند.
x = 10
s = "Hello"
به طور کلی، زبانهای “سطح بالا” مانند Python برای توسعهدهندگان بسیار راحتتر هستند. اما زمانی که کد اجرا میشود، نیاز به تبدیل شدن به دستورات سطح پایین دارد. این تبدیل زمان بیشتری میبرد که به نفع راحتی استفاده فدا میشود.
اگر زمان عامل مهمی باشد، باید از دستورات سطح پایین استفاده کنید. بنابراین، به جای نوشتن کد با Python که رابط کاربری است، میتوانید آن را با استفاده از CPython که پشتصحنه Python است و در C پیادهسازی شده است، بنویسید. با این حال، اگر این کار را انجام دهید، احساس خواهید کرد که در حال برنامهنویسی به زبان C هستید، نه Python.
CPython بسیار پیچیدهتر است. در CPython، همه چیز در C پیادهسازی شده است. هیچ راهی برای فرار از پیچیدگی C در کدنویسی وجود ندارد. به همین دلیل بسیاری از توسعهدهندگان به جای آن از Cython استفاده میکنند. اما Cython چگونه با CPython متفاوت است؟
Cython چگونه متفاوت است
طبق تعاریف بالا، Cython یک زبان است که به شما اجازه میدهد بهترینهای هر دو دنیای سرعت و سهولت استفاده را داشته باشید. شما هنوز میتوانید کد معمولی را در پایتون بنویسید، اما برای تسریع زمان اجرا، Cython به شما این امکان را میدهد که برخی بخشهای کد پایتون را با C جایگزین کنید. بنابراین، در نهایت هر دو زبان را در یک فایل ترکیب میکنید. توجه داشته باشید که میتوانید تصور کنید که همه چیز در پایتون در Cython معتبر است، اما با برخی محدودیتها.
فایل پایتون معمولی پسوند .py دارد، اما فایل Cython پسوند .pyx دارد. همان کد پایتون میتواند داخل فایلهای .pyx نوشته شود، اما این فایلها همچنین به شما اجازه میدهند از کد Cython استفاده کنید. توجه داشته باشید که قرار دادن کد پایتون در یک فایل .pyx ممکن است فرآیند را سریعتر از اجرای کد پایتون به طور مستقیم کند، اما به اندازه زمانی که نوع متغیرها را اعلام میکنید، سریع نخواهد بود. بنابراین، تمرکز این آموزش نه تنها بر روی نوشتن کد پایتون در داخل فایل .pyx، بلکه بر روی انجام ویرایشهایی است که باعث سریعتر اجرا شدن آن میشود. با این کار ما کمی سختی به برنامهنویسی اضافه میکنیم، اما زمان زیادی از این طریق صرفهجویی میشود. اگر تجربهای در برنامهنویسی C دارید، این کار برای شما آسانتر خواهد بود.
سایترون کردن کد ساده پایتون
برای تبدیل کد پایتون به Cython، ابتدا باید فایلی با پسوند .pyx
ایجاد کنید به جای پسوند .py
. در داخل این فایل، میتوانید شروع به نوشتن کد معمولی پایتون کنید (توجه داشته باشید که برخی محدودیتها در کدی که Cython میپذیرد وجود دارد که در مستندات Cython توضیح داده شده است).
قبل از ادامه، مطمئن شوید که Cython نصب شده باشد. میتوانید این کار را با استفاده از دستور زیر انجام دهید.
pip install cython
برای تولید فایل .pyd/.so ابتدا باید فایل Cython را بسازیم. فایل .pyd/.so نمایانگر مدولی است که بعداً باید وارد شود. برای ساختن فایل Cython از یک فایل setup.py استفاده میشود. این فایل را ایجاد کرده و کد زیر را در آن قرار دهید. از تابع distutils.core.setup() برای فراخوانی تابع Cython.Build.cythonize() استفاده خواهیم کرد، که فایل .pyx را سایترون میکند. این تابع مسیر فایلی که میخواهید آن را سایترون کنید، میپذیرد. در اینجا فرض کردهام که فایل setup.py در همان محلی که فایل test_cython.pyx قرار دارد، قرار دارد.
import distutils.core
import Cython.Build
distutils.core.setup(
ext_modules = Cython.Build.cythonize("test_cython.pyx"))
برای ساختن فایل Cython، دستور زیر را در خط فرمان وارد کنید. انتظار میرود که دایرکتوری فعلی خط فرمان همان دایرکتوری فایل setup.py باشد.
python setup.py build_ext --inplace
پس از اتمام این دستور، دو فایل در کنار فایل .pyx قرار خواهند گرفت. اولین فایل دارای پسوند .c است و فایل دیگر پسوند .pyd (یا مشابه آن، بسته به سیستم عامل استفاده شده) خواهد داشت. برای استفاده از فایل تولید شده، کافی است ماژول test_cython را وارد کنید و پیغام “Hello Cython” به طور مستقیم نمایش داده خواهد شد، همانطور که در زیر مشاهده میکنید.
ما اکنون کد پایتون را با موفقیت سایتونیزه کردهایم. بخش بعدی به سایتونیزه کردن یک فایل .pyx که در آن یک حلقه ایجاد شده است، پرداخته است.
سایتونیزه کردن یک حلقه “for”
حال بیایید کار قبلی خود را بهینهسازی کنیم: یک حلقه for که از میان 1 میلیون عدد عبور کرده و آنها را جمع میکند. بیایید با بررسی کارایی فقط خود حلقه شروع کنیم. برای برآورد مدت زمانی که طول میکشد تا اجرا شود، ماژول time وارد میشود.
import time
t1 = time.time()
for k in range(1000000):
pass
t2 = time.time()
t = t2-t1
print("%.20f" % t)
در یک فایل .pyx، زمان متوسط برای 3 بار اجرا 0.0281 ثانیه است. کد روی ماشینی با پردازنده Core i7-6500U @ 2.5 GHz و 16 گیگابایت حافظه RAM DDR3 در حال اجرا است.
این را با زمانی که برای اجرا در یک فایل پایتون معمولی صرف میشود مقایسه کنید، که میانگین آن 0.0411 ثانیه است. این بدین معناست که Cython برای تکرارها تنها 1.46 برابر سریعتر از پایتون است، حتی بدون اینکه نیاز باشد حلقه for را برای اجرا با سرعت C تغییر دهیم.
حال بیایید کار جمعکردن را اضافه کنیم. برای این کار از تابع range() استفاده خواهیم کرد.
import time
t1 = time.time()
total = 0
for k in range(1000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)
توجه داشته باشید که هر دو اسکریپت همان مقدار را باز میگردانند، که برابر با 499999500000 است. در پایتون این کار به طور متوسط 0.1183 ثانیه طول میکشد تا اجرا شود (بین سه آزمایش). در Cython این مقدار 1.35 برابر سریعتر است و به طور متوسط 0.0875 ثانیه طول میکشد.
حال بیایید یک مثال دیگر ببینیم که در آن حلقه از 0 شروع کرده و از میان 1 میلیارد عدد عبور میکند.
import time
t1 = time.time()
total = 0
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.20f" % t)
اسکریپت Cython تقریباً در 85 ثانیه (1.4 دقیقه) به پایان رسید، در حالی که اسکریپت پایتون تقریباً در 115 ثانیه (1.9 دقیقه) به پایان رسید. در هر دو حالت، زمان زیادی صرف میشود. استفاده از Cython چه فایدهای دارد اگر زمان انجام چنین کاری سادهای بیش از یک دقیقه طول بکشد؟ توجه داشته باشید که این اشتباه ماست، نه Cython.
همانطور که قبلاً گفته شد، نوشتن کد پایتون درون اسکریپت Cython (.pyx) یک بهبود است، اما تغییر قابل توجهی در زمان اجرا ایجاد نمیکند. ما باید تغییراتی در کد پایتون داخل اسکریپت Cython اعمال کنیم. اولین نکتهای که باید به آن توجه کنیم، تعیین نوع دادهای متغیرهای استفاده شده به طور صریح است.
تخصیص نوع داده C به متغیرها
طبق کد قبلی، 5 متغیر استفاده شدهاند: total، k، t1، t2، و t. نوع داده تمام این متغیرها به طور ضمنی توسط کد استنباط میشود، بنابراین زمان بیشتری میبرد. برای صرفهجویی در زمانی که برای استنباط نوع دادهها استفاده میشود، بیایید نوع دادهها را از زبان C تخصیص دهیم.
نوع متغیر total از نوع unsigned long long int است. این یک عدد صحیح است زیرا مجموع تمام اعداد یک عدد صحیح است و unsigned است زیرا مجموع همیشه مثبت خواهد بود. اما چرا long long؟ چون مجموع تمام اعداد بسیار بزرگ است، بنابراین از long long استفاده میشود تا اندازه متغیر به حداکثر اندازه ممکن برسد.
نوع داده تعیین شده برای متغیر k int است و برای سه متغیر باقیمانده t1، t2، و t نوع داده float اختصاص داده میشود.
import time
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
t1 = time.time()
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)
توجه داشته باشید که دقت تعریف شده در آخرین دستور print به 100 تنظیم شده است و تمام این اعداد صفر هستند (به تصویر بعدی مراجعه کنید). این چیزی است که میتوانیم از استفاده از Cython انتظار داشته باشیم. در حالی که پایتون بیش از 1.9 دقیقه طول میکشد، Cython هیچ زمانی را صرف نمیکند. حتی نمیتوانم بگویم که سرعت آن 1000 یا 100000 برابر سریعتر از پایتون است؛ من دقتهای مختلفی برای زمان چاپ شده امتحان کردم و هنوز هیچ عددی ظاهر نمیشود.
توجه داشته باشید که شما همچنین میتوانید یک متغیر صحیح (integer) برای نگهداری مقداری که به تابع range() ارسال میشود، ایجاد کنید. این کار عملکرد را حتی بیشتر بهبود میبخشد. کد جدید در زیر آمده است، جایی که مقدار در متغیر صحیح maxval ذخیره میشود.
import time
cdef unsigned long long int maxval
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
maxval=1000000000
t1=time.time()
for k in range(maxval):
total = total + k
print "Total =", total
t2=time.time()
t = t2-t1
print("%.100f" % t)
حالا که نحوه افزایش عملکرد اسکریپتهای پایتون با استفاده از سایتون را دیدیم، بیایید این را در Raspberry Pi (RPi) اعمال کنیم.
دسترسپذیری Raspberry Pi از کامپیوتر شخصی
اگر این اولین باری است که از Raspberry Pi خود استفاده میکنید، باید هم کامپیوتر شخصی و هم RPi از طریق یک شبکه به هم متصل شوند. شما میتوانید این کار را با اتصال هر دو دستگاه به یک سوئیچ انجام دهید که در آن DHCP (پروتکل پیکربندی میزبان پویا) فعال باشد تا به طور خودکار آدرسهای IP را اختصاص دهد. پس از ایجاد موفق شبکه، میتوانید به RPi بر اساس آدرس IPv4 اختصاص داده شده به آن دسترسی پیدا کنید. چگونه میتوان فهمید که آدرس IPv4 اختصاص داده شده به RPi چیست؟ نگران نباشید، شما به سادگی میتوانید از ابزار اسکنر IP استفاده کنید. در این آموزش، من از یک اپلیکیشن رایگان به نام Advanced IP Scanner استفاده میکنم.
رابط کاربری این اپلیکیشن به صورت زیر است. این اپلیکیشن یک بازه از آدرسهای IPv4 را برای جستجو میپذیرد و اطلاعات دستگاههای فعال را بازمیگرداند.
شما باید بازه آدرسهای IPv4 در شبکه محلی خود را وارد کنید. اگر از این بازه اطلاع ندارید، کافیست دستور ipconfig را در ویندوز (یا ifconfig را در لینوکس) اجرا کنید تا آدرس IPv4 کامپیوتر خود را پیدا کنید (همانطور که در شکل زیر نشان داده شده است). در حالت من، آدرس IPv4 اختصاص داده شده به آداپتور Wi-Fi کامپیوتر من 192.168.43.177 و ماسک زیرشبکه 255.255.255.0 است. این بدین معناست که بازه آدرسهای IPv4 در شبکه از 192.168.43.1 تا 192.168.43.255 است. طبق شکل، آدرس IPv4 192.168.43.1 به دروازه (Gateway) اختصاص داده شده است. توجه داشته باشید که آخرین آدرس IPv4 در این بازه، یعنی 192.168.43.255، برای پیامهای پخش (broadcast) رزرو شده است. بنابراین، بازهای که باید جستجو کنید از 192.168.43.2 شروع شده و به 192.168.43.254 خاتمه مییابد.
طبق نتیجه اسکن که در شکل بعدی نشان داده شده است، آدرس IPv4 اختصاص داده شده به RPi، 192.168.43.63 است. این آدرس IPv4 میتواند برای ایجاد یک جلسه شل امن (SSH) استفاده شود.
برای برقراری جلسه SSH، من از نرمافزار رایگان به نام MobaXterm استفاده خواهم کرد. رابط کاربری این برنامه به صورت زیر است.
برای ایجاد یک جلسه SSH، کافی است روی دکمه Session در گوشه بالا-چپ کلیک کنید. یک پنجره جدید همانطور که در زیر نشان داده شده است، ظاهر خواهد شد.
از این پنجره، روی دکمه SSH در گوشه بالا-چپ کلیک کنید تا پنجرهای که در زیر نشان داده شده است باز شود. فقط آدرس IPv4 رزبری پای و نام کاربری (که به طور پیشفرض pi است) را وارد کنید، سپس روی OK کلیک کنید تا جلسه آغاز شود.
پس از کلیک روی دکمه OK، پنجرهای جدید ظاهر میشود که از شما خواسته میشود رمز عبور را وارد کنید. رمز عبور پیشفرض raspberrypi است. پس از ورود به سیستم، پنجره بعدی ظاهر میشود. بخش سمت چپ به شما کمک میکند تا به راحتی در دایرکتوریهای رزبری پای حرکت کنید. همچنین یک خط فرمان برای وارد کردن دستورات وجود دارد.
استفاده از Cython با Raspberry Pi
یک فایل جدید ایجاد کنید و پسوند آن را به .pyx تغییر دهید تا کد مثال قبلی را بنویسید. در نوار پنل سمت چپ گزینههایی برای ایجاد فایلها و دایرکتوریهای جدید وجود دارد. میتوانید از آیکون فایل جدید برای سادهتر کردن این کار استفاده کنید، همانطور که در تصویر زیر نشان داده شده است. من فایلی به نام test_cython.pyx در دایرکتوری اصلی رزبری پای ایجاد کردم.
فایل را دوبار کلیک کنید تا آن را باز کنید، کد را بچسبانید و ذخیره کنید. سپس باید فایل setup.py را ایجاد کنیم که دقیقاً همانطور که قبلاً بحث کردیم، خواهد بود. پس از آن باید دستور زیر را برای ساخت اسکریپت Cython صادر کنیم.
python3 setup.py build_ext --inplace
پس از اینکه این دستور با موفقیت کامل شد، میتوانید فایلهای خروجی را در پانل سمت چپ پیدا کنید، همانطور که در شکل بعدی نشان داده شده است. توجه داشته باشید که پسوند ماژول برای وارد کردن اکنون .so است، زیرا دیگر از ویندوز استفاده نمیکنیم.
حالا بیایید پایتون را فعال کرده و ماژول را وارد کنیم، همانطور که در زیر نشان داده شده است. همان نتایج بدست آمده در کامپیوتر شخصی در اینجا نیز بدست میآید؛ زمان مصرفی عملاً صفر است.
نتیجه
این آموزش به بررسی نحوه استفاده از Cython برای کاهش زمان محاسباتی اجرای اسکریپتهای پایتون پرداخت. ما مثالی از استفاده از یک حلقه for
برای جمع کردن همه عناصر یک لیست پایتون با 1 میلیارد عدد را بررسی کردیم و زمان اجرای آن را با و بدون اعلام نوع متغیرها مقایسه کردیم. در حالی که این فرآیند در پایتون خالص نزدیک به دو دقیقه طول میکشد، با استفاده از Cython و اعلام متغیرهای ایستا، این فرآیند عملاً بدون زمان اجرا میشود.