چرا پایتون کُند است؟
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 9 دقیقه

چرا پایتون کُند است؟

پایتون زبان برنامه‌نویسی بسیار محبوبی‌ است. از پایتون می‌شود برای DevOps، Data Science، توسعه وب و امنیت استفاده کرد.

با این حال نمی‌توان مدال خوبی برای سرعت به آن داد.

چگونه می‌شود یک مقایسه سرعت دقیق روی زبان‌های مختلفی مانند جاوا، سی‌پلاس‌پلاس، سی شارپ و یا پایتون را انجام داد؟ باید بگویم که هیچ بنچمارک دقیقی وجود ندارد چرا که بسته به نوع اپلیکیشن، نتایج مختلف خواهند بود. اما در چنین شرایطی باز هم Computer Language Benchmarks می‌تواند نقطه شروع خوبی باشد.

من برای مدت‌هاست که یکی از اپلیکیشن‌های داخل این وبسایت را برای زبان‌های برنامه‌نویسی مختلف بررسی می‌کنم و باید بگویم که برای اجرای آن پایتون کند‌ترین زبان در بین لیست زبان‌ها بود. این موضوع برای دیگر زبان‌های مفسری مانند جاوااسکریپت نیز به همین صورت بود. بعد از آن کامپایلرهای JIT که زبان‌های سی‌شارپ و جاوا از آن استفاده می‌کنند و در نهایت سریع‌ترین زبان‌ها آن‌هایی بودند که از کامپایلر AOT بهره می‌گرفتند، سی و سی‌پلاس‌پلاس از این دست زبان‌ها بودند.

سه دلیل برای آنکه باعث می‌شود پایتون زبانی کند باشد عبارت است از:

  1. GIL یا Global Interpreter Lock
  2. به دلیل آنکه کدها بجای کامپایل شدن تفسیر می‌شوند.
  3. به دلیل پویا بودن.

اما کدام یک از این سه مورد بیشترین تاثیر را روی سرعت پایین پایتون دارد؟

1. بررسی GIL

کامپیوترهای مدرن همراه با پردازنده‌هایی عرضه می‌شوند که معمولا چندین هسته مختلف دارند. برای آنکه بشود از قدرت این هسته‌ها بهره برد، سیستم‌ عامل یک ساختار سطح پایین به نام thread یا رشته را ایجاد می‌کند. اینجا جایی‌ست که یک پردازش یا Process «برای مثال مرورگر کروم» می‌تواند در بین چندین رشته مختلف اجرا شود. به همین دلیل است که در چنین حالتی پردازنده می‌تواند کارایی بسیار بیشتری از خود نشان بدهد و اجرای اپلیکیشن را سریع‌تر نماید.

مرورگر کروم تا به اینجای کار ۴۴ رشته باز دارد. البته در نظر بگیرید که ساختار و API رشته‌ها بسته به سیستم عامل‌های مختلف ممکن است متفاوت باشد.

اگر با مبحث برنامه‌نویسی چند نخی آشنایی ندارید باید بگویم که قبل از جلو رفتن بیشتر نیاز است که شما را با مفهوم locks آشنا کنیم. فارغ از پردازش‌های تک نخی، در پردازش‌های چند نخی اگر بخواهید مقدار یک متغیر را تغییر دهید باید مطمئن شوید که نخ‌ها یا رشته‌های مختلف همزمان به آدرس آن متغیر دسترسی پیدا نخواهند کرد و در نهایت همزمان برای تغییر آن متغیر تلاش نمی‌کنند. 

زمانی که در Cpython (یک پیاده‌سازی از پایتون) یک متغیر تعریف می‌شود، مفسر یک قسمت از حافظه را به آن اختصاص داده و شروع به تعداد ارجاعاتی می‌کند که به آن متغیر شده، این پروسه را Reference Counting نیز می‌گویند. اگر تعداد این ارجاعات برابر با صفر باشد مفسر آن قسمت از حافظه را که به متغیر اختصاص داده بود آزاد می‌کند. به همین دلیل است که اگر یک متغیر موقت را در یک حلقه For ایجاد می‌کنید، حافظه کامپیوتر زیاد درگیر نمی‌شود.

اما چالش زمانی آغاز می‌شود که متغیرها بین رشته‌های مختلفی به اشتراک گذاشته می‌شوند، این دقیقا جایی‌ست که پایتون سعی دارد تا فرایند Reference Counting را Lock نماید. برای چنین کاری یک GIL یا Global Interpreter Lock وجود دارد که به دقت چنین موضوعی را کنترل می‌کند. با اعمال چنین حالتی روی یک برنامه، مفسر تنها می‌توند یک عملیات را در یک زمان انجام دهد و دیگر توجهی به تعداد رشته‌های موجود ندارد.

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

اگر شما یک اپلیکیشن تک مفسره و یا تک رشته داشته باشید در نهایت هیچ تفاوتی در فرایند سرعت ندارید. حذف کردن GIL نیز نمی‌تواند تاثیر زیادی روی کارایی کدهای‌تان داشته باشد.

اگر بخواهید فرایند همزمانی را روی یک مفسر با استفاده از حالت چند نخی پیاده‌سازی کنید، اگر نخ‌های شما با فرایندهای IO بسیاری درگیر شود (برای مثال در شبکه و یا روی یک دیسک) ممکن است عواقب بدی برای GIL را مشاهده بکنید. در زیر یک گراف وجود دارد: بلوک‌های قرمز رنگ نشان‌گر میزان شکست‌های دو نخ برای انجام یکسری پروسه هستند که سیستم عامل آن‌ها را زمان‌بندی کرده. اما زمانی که سیستم عامل در زمان x به رشته ۱ گفته که فلان کار را انجام بدهد، بدلیل مشغول بودن رشته ۲ این کار انجام نشده و در نهایت شکست خورده است.

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

در رابطه با دیگر پیاده‌سازی‌های پایتون چطور؟

PyPy یکی دیگر از پیاده‌سازی‌ پایتون است که دارای GIL بوده و تقریبا سه برابر Cpython سریع است.

Jython به لطف استفاده از سیستم مدیریت حافظه JVM سریع‌تر عمل کرده و نیازی نیز به GIL ندارد.

اگر بخواهیم جاوااسکریپت را نیز به عنوان یک زبان در سطح پایتون به شمار بیاوریم، باید بگوییم که جاوااسکریپت از معقوله Mark-and-Sweep Garbage Collection برای مدیریت حافظه خود استفاده می‌کند.

۲. به دلیل آنکه کدها بجای کامپایل شدن تفسیر می‌شوند

شیوه‌ای که Cpython با کدها برای اجرا کار می‌کند چندان خوب نیست. تصور کنید زمانی که شما دستور python myscript.py را اجرا می‌کنید، پایتون یک رشته بزرگ از فرایندهای خواندن، تفسیر کردن، کامپایل کردن و اجرا کردن را انجام می‌دهد.

یک نقطه مهم از این فرایند مربوط به ایجاد فایل‌ .pyc است که در فرایند کامپایل کردن به وجود می‌آید. این فایل حاوی یکسری بایت کد است، اما نه فقط بایت کدهای شما، تمام ماژول‌هایی که import کرده‌اید نیز در این قسمت قرار می‌گیرد.

برخی اوقات پایتون تنها از طریق این بایت کدها برنامه را اجرا می‌کند.

زبان برنامه‌نویسی جاوا از طرفی دیگر برای اجرای برنامه‌های خود ابتدا آن‌ها را به یک زبان سطح میانی تبدیل کرده و سپس از طریق کامپایل به صورت JIT آن‌ها را به کدهای ماشین تبدیل می‌کند. زبان‌های .NET نیز به همین شکل عمل می‌کنند.

اما اگر قرار باشد که همه آن‌ها یک پروسه کامپایل و تبدیل به بایت کد را داشته باشند چرا پایتون از آن‌ها کندتر است؟ خب دلیل این موضوع وجود حالت کامپایلی JIT در زبان‌های جاوا و .NET است.

البته خود JIT باعث سریع‌تر شدن روند اجرا نمی‌شود چرا که در هر صورت وی یک مجموعه بایت کد را اجرا می‌کند، اما کار مهمی را که JIT انجام می‌دهد بهینه‌سازی بخش‌هایی از نرم‌افزار است که تعداد بار زیادی اجرا می‌شود. 

همچنین این موضوع را در نظر بگیرید که جاوا و .NET زبان‌های پویا نیستند و برای تعیین نوع دیتا از کامپایلر در زمان اجرا کمک نمی‌گیرند، بنابراین مدت زمان بیشتری را ذخیره می‌کنند.

چرا CPython از JIT استفاده نمی‌کند؟

JIT یک مشکل بزرگ دارد و آن زمان اجرا شدن است. زمان اجرا شدن Cpython به خودی خود کند است حال اگر از JIT نیز استفاده شود این روند بسیار کندتر خواهد شد. برای اثبات چنین قضیه‌ای می‌توانید به PyPy مراجعه کنید. همانطور که قبلا اشاره شد PyPy یک پیاده‌سازی از پایتون است، اما در این پیاده‌سازی یک تفاوت دیگر وجود دارد و آن وجود JIT است. PyPy نسبت به Cpython دو تا سه بار برای اجرا کندتر است.

۳. به دلیل پویا بودن

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

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

a = 1
a = "foo"

در این مثال پایتون ابتدا متغیری با نام a با مقدار عددی 1 تعیین کرده و سپس آن را خارج نموده و سپس نوع داده‌ای جدیدی که از نوع str است را در متغیر قرار می‌دهد.

زبان‌های ایستا چنین حالتی را ندارند. آن‌ها درست به صورتی ساخته شده‌اند که بیشترین سازگاری را با پردازنده داشته باشند.

طراحی کلی پایتون کاری کرده است که روند بهینه‌سازی آن بسیار سخت باشد و در نهایت این موضوع روی سرعت برنامه نیز تاثیر گذاشته است. 

خب آیا پویا بودن روی سرعت پایتون تاثیرگذار است:

  • مقایسه و تبدیل نوع‌های داده‌ای مختلف هزینه‌بر است، هر زمانی که یک متغیر خوانده می‌شود نیاز است که نوع آن نیز تعیین شود.
  • بهینه‌سازی زبان‌های پویا بسیار سخت است. 
  • اگر می‌خواهید از این موضوع به خوبی عبور کنید از Cython استفاده نمایید. Cython یک گزینه مناسب فراهم کردن قابلیت استاتیک-تایپ در پایتون است.

در پایان

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

با این حال اگر می‌خواهید اپلیکیشنی بنویسید که سریع باشد اما زمان اجرای اولیه آن برای‌تان مهم نیست PyPy را در نظر بگیرید. و اگر به دنبال قابلیت نوع داده‌ای استاتیک هستید Cython انتخابی مناسب است.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
4.5 از 2 رای

/@arastoo
ارسطو عباسی
کارشناس تولید و بهینه‌سازی محتوا

کارشناس ارشد تولید و بهینه‌سازی محتوا و تکنیکال رایتینگ - https://arastoo.net

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید