مقایسۀ توابع حلقه در PHP

زبانِ PHP، توابع و دستورات مختلفی برای کار با حلقه‌ها و آرایه‌ها در اختیار دارد که در وضعیت‌های مختلف و برای حل مشکلات مختلف می‌توانید از هر یک از آن‌ها استفاده کنید. در نگاه اول به نظر می‌رسد که تمام آن‌ها، کاری مشابه هم انجام می‌دهند. همۀ آن‌ها در یک حلقۀ متناهی دستوراتی را برای حصول نتیجه انجام می‌دهند! در زیر به تعدادی از مشهورترینِ این توابع و دستورات اشاره‌شده است:

فارغ از اینکه هرکدام از توابعِ فوق چه خصوصیات و امکاناتی را در اختیار شما قرار می‌دهند، آیا تابه‌حال به این فکر کرده‌اید که برای یک تکلیفِ مشخص، کدام‌یک سریع‌تر و بهینه‌تر هستند؟

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

سال‌ها پیش وقتی در حال یادگیری زبان PHP بودم تصور می‌کردم که تمام این توابع یک‌جور عمل می‌کنند. با همۀ آن‌ها می‌توانم درون یک آرایه قدم زده و دستورات لازم را اجرا کنم. درعین‌حال تصور می‌کردم، توابعِ توکارِ PHP قاعدتاً باید سریع‌تر باشند. زمان زیادی لازم نبود تا به نادرستی تصور خود پی ببرم. امروز و پس از گذشت سال‌ها از آن ماجرا، در حین کار به وضعیتی مشابه برخورد کردم که من را برای نوشتن این مطلب ترغیب کرد.

برای درک بیشتر و مقایسۀ بهتر این توابع و دستورات، اجازه دهید فرض کنیم که یک مجموعۀ آرایه‌ای با یک‌میلیون عضو در اختیارداریم. در ادامه، فرآیندِ حلقه زدن بین این یک‌میلیون عضو، با استفاده از هر یک از توابع و دستورات فوق را باهم مقایسه خواهیم کرد. هدف، مقایسۀ سرعتِ هر یک از توابع و دستورات در اجرای حلقه است. میزان حافظه‌ای که هر یک در انجام این فرآیند صرف می‌کنند نیز حائز اهمیت است.

حلقه php
تمام کدهای موجود در این یادداشت را می‌توانید در اینجا ببینید.

خطِ اول به PHP می‌گوید که حافظۀ مُجاز خود را به ۵۰۰ مگابایت افزایش دهد تا با مشکل کمبودِ حافظه مواجه نشویم. خطوط بعدی آرایه‌ای به طول یک‌میلیون، ایجاد می‌کند.

به‌منظورِ حصولِ بهترین و واقعی‌ترین نتیجه، سعی شده است تا از کمترین کدِ ممکن برای ایجاد تست‌ها استفاده شود. از متغیرهای کمتر، دستورات کمتر و کمترین عملیاتِ ممکن در حلقه استفاده‌شده است. همان‌طور که بینید، درونِ حلقه‌ها هیچ اتفاقی رخ نمی‌دهد زیرا هدفِ اصلی، اجرای خودِ حلقه به‌صورت خام است.

درجایی از دیسک، یک فایل به نام loop_speed_mem_test.php بسازید و کدهای زیر را درون آن تایپ کنید!

تایپ کنم؟! شوخی می‌کنی؟ چرا خیلی راحت Copy/Paste نکنم؟

خب، قطعاً برداشتن یک‌بارۀ کدها و قرار دادنشان در فایل کار راحت‌تری است. من از فعلِ تایپ کردن استفاده می‌کنم، چون به نظرم در دنیای برنامه‌نویسی وقتی نمونۀ کدی را مجدداً خودتان تایپ می‌کنید، منطق و خروجی آن را بهتر یاد خواهید گرفت. به‌هرحال همیشه در Copy/Paste آزادید! 🙂

حلقه های PHP

به علت اینکه قصد داریم مقدار حافظۀ مصرفی هر تابع را نیز محاسبه کنیم، بنابراین اجرای یکجا و پشت سر همِ همۀ دستورات، نتیجۀ صحیحی را به بار نخواهد آورد. حافظۀ مصرفی هر تابع با حافظۀ مصرفی تابعِ قبل ادغام‌شده و نتایج را مخدوش خواهد کرد. برای جلوگیری از این مشکل، حافظۀ PHP باید در ابتدای هر دستور بازگردانی شود بنابراین چاره‌ای به‌جز اجرای جداگانۀ تک‌تک دستورات نداریم. به این منظور از دستور goto برای بلاک بندی هر آزمون استفاده‌شده است و با تغییر مقدار دستورِ goto در ابتدای فایل می‌توانید هر بلاک را جداگانه اجرا کنید.

نکته در خصوص تکنیک ارجاع در foreach: برای تغییر در مقادیرِ هر یک از عناصرِ یک آرایه – بدون تعریف آرایۀ واسط، می‌توانید از ترفندِ ارجاع، در حلقۀ foreach استفاده کنید. این ترفند به‌شدت بهینه و حافظه دوست است! (به قطعۀ foreach_loop_ref در کدِ فوق دقت کنید.)

مقدارِ برچسبِ goto را به ترتیب بر روی array_filter، array_map، array_walk، foreach_loop، foreach_loop_ref، for_loop، while_loop تنظیم کرده، هر بار فایل را ذخیره و آن را در خط فرمان اجرا کنید:

حلقه های PHP

خروجی ترمینال برای array_filter، چیزی شبیه این خواهد بود:

حلقه های PHP

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

حلقه های PHP

احتمالاً شما هم مثلِ من شگفت‌زده شده‌اید. اختلاف‌ها، هم در ستون سرعت و هم در ستون حافظه، چشم‌گیر و قابل‌تأمل هستند. ضرب‌المثلی هست که می‌گوید: «یک عکس از هزاران کلمه بهتر است». آنها را روی نمودار می‌برم:

پرفورمنس با آرایۀ یک میلیونی
پرفورمنس با آرایۀ یک میلیونی

امیدوارم هنوز نتیجه‌گیری نکرده باشید! بااینکه به نظر می‌رسد اعداد و نمودارها، پاسخ صحیح را فریاد می‌زنند!، اما همچنان چند نکتۀ دیگر برای اینکه بتوانید در آینده تصمیم بگیرید که از کدام روش استفاده کنید، باقی‌مانده است.

خب، اگر تعداد عناصرِ آرایه را کمتر کنیم چه اتفاقی می‌افتد؟ مثلاً اگر به‌جای استفاده از یک آرایۀ یک‌میلیون عضوی، از یک آرایۀ صد هزار عضوی یا کمتر استفاده کنیم، چه؟! آیا اعداد و ارقام تغییر خواهند کرد؟ نمودارهای زیر نمایانگرِ نتایج برای یک آرایۀ صد هزار عضوی است:

پرفورمنس با آرایۀ ۱۰ هزارتایی
پرفورمنس با آرایۀ ۱۰۰ هزارتایی

همان‌طور که مشاهده می‌کنید تمامِ مقادیر، کاهش محسوسی داشته و به شکل معنی‌داری همگی حدود ۱۰ برابر کمتر شده‌اند. کمترین سرعت به زیر ۰/۶ ثانیه و بیشترین حافظۀ مصرفی به زیر ۱۰ مگابایت تقلیل یافته است. البته تغییری در نسبت‌ها و رتبه‌ها ایجاد نشده است. توابع سریع، همچنان سریع و توابع کند، همچنان کند باقی‌مانده‌اند!

سخن پایانی

خب، پس باید کار با آن‌ها را فراموش کرد؟ یقیناً نه!

اگر به نمودارهای مربوط به آرایه‌های یک‌میلیونی و صد هزار عنصری توجه کنید، متوجه افتِ چشم‌گیر زمان مصرفی هر تابع خواهید شد. در حلقه‌ها و آرایه‌های خیلی بزرگ، توجه به بهینه بودن کد، مصرفِ حافظه و سرعتِ عملیات در درجۀ اول اهمیت قرار دارند، بنابراین از دستوراتی که سریع‌تر و بهینه‌تر هستند استفاده کنید.

تصور کنید اگر عناصرِ آرایه را در مثالِ فوق کوچک‌تر کنیم چه روی خواهد داد؟ مثلاً طولِ آن را به ۱۰۰ کاهش دهیم! خواهید دید که اختلاف‌ها به‌شدت کاهش‌یافته و قابل‌چشم‌پوشی است. از زاویۀ دیدِ کاربر و نرم‌افزار، در محیطی متعارف، اختلافِ ۰/۰۰۰۵۲ ثانیه و ۰/۰۰۰۰۳۲ ثانیه (تفاوت سرعت بین تابع array_filter و دستورِ foreach برای آرایۀ ۱۰۰ تایی) مطلقاً محسوس نیست! تجربه نشان داده است که در بیشترِ پروژه‌ها با آرایه‌ها و حلقه‌های خیلی کوچک و متوسط سروکار خواهیم داشت، بنابراین در این مواقع می‌توانید از سادگی، خوانایی و قابلیت‌های منحصربه‌فرد توابعِ توکارِ PHP بهره گرفته و نگرانِ کارایی و سرعت کد خود نباشید.

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

👋

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *