سرعت در دنیای وب حرف اول رو میزنه، هر چه قدر که وب سایت شما سریع تر باشه کاربر پسند تره.
بیشتر مواقع ما برای سریع تر کردن لود وب سایتمون به سراغ راه حل هایی مثل مینیمایز کردن فایلهای css و js ، استفاده از سیستم ها و کتابخانه های Cache، کم حجم کردن تصاویر و ... می رویم. ولی یادمون می رود که به نحوه کدنویسی خومون هم توجه داشته باشیم تا شاید بهینه تر بشه کد زد. نکته های خیلی ریزی در نحوه نوشتن کدهای PHP است که رعایت کردن آنها در نهایت باعث افزایش کارایی می شوند. یکی از این موارد استفاده از generator ها است. که امروز با هم دیگه بررسی میکنیم.
جتریتور (Generator) چیست ؟
جنریتورها (Generators) که از نسخه 5.5 به PHP اضافه شدند یک نوع فانکشن هستند که امکان پیاده سازیiterator ها یا همان تکرار کننده های (مثلfor, foreach, while) بهینه رو به ما می دهند بدون اینکه حافظه زیادی رو اشغال کنند و یا باعث پیجیده تر شدن کد شوند.
چرا به Generator ها نیاز داریم ؟
خب اجازه بدهید این را با یک مثال ملموس بیان کنیم. در این مثال یک رنجی(محدوده ای) از رشته ها تولید کنیم
<?php function getRange ($max = 10) { $array = []; for ($i = 1; $i < $max; $i++) { $array[] = $i; } return $array; } foreach (getRange(15) as $range) { echo "Dataset {$range} <br>" }
خب اگر اون فایل رو در لوکال هاست اجرا کنیم، خواهیم دید که خروجی شبیه زیر خواهد داشت.
Dataset 1 Dataset 2 Dataset 3 Dataset 4 Dataset 5 Dataset 6 Dataset 7 Dataset 8 Dataset 9 Dataset 10
خب خروجی فوق خیلی ساده است و نیاز به توضیح خاصی نداره و کار خاص و سنگینی هم انجام نمیده ولی حالا بیایید برگردیم و یک تغییرکوچک روی کد خودمون داشته باشیم
<?php foreach (getRange(PHP_INT_MAX) as $range) { echo "Dataset {$range} <br>" }
حالا بالاترین رنج تولید شده برابر با PHP_INT_MAX است که بزرگترین عددی است که نسخه PHP شما می تواند به آن برسد. بعد از اعمال این تغییر به صفحه مرورگر خود برگشته و صفحه مثال رو refresh کنید.
این بار متواجه خواهید شد که با خطای fatal error زیر مواجه می شوید
خب زیاد جالب نیست نه! PHP حافظه کم آورد! آسان ترین و راحت ترین روشی که به فکر بعضی عزیزان می رسه افزایش مقدار memory_limit در فایل php.ini است. ولی واقعا این کار غیر حرفه ای است، با این کار اجازه می دهیم که یک اسکریپت کوچک تمام حافظه ما رو اشغال کند!
راه حل: استفاده از Generator ها
بیاید تابع بالا رو مجدد با مقدار PHP_INT_MAX تعریف کنیم با این تفاوت که این بار یک generator ایجاد خواهیم کرد.
<?php function getRange ($max = 10) { for ($i = 1; $i < $max; $i++) { yield $i; } } foreach (getRange(PHP_INT_MAX) as $range) { echo "Dataset {$range} <br>" }
خب حالا اگر این دفعه صفحه خودتون رو رفرش کنید خواهید دید که بدون هیچ مشکلی در سرعت و کمبود حافظه کدهاتون اجرا می شوند.
یک مثال ساده دیگه از جنریتورها میتونه دوباره نوشتن تابع range خود PHP باشه. این تابع دو پارامتر میگیره و باید یک آرایه از مقدار های بین آن دو رو تولید کنه و برگردونه که میتونه در نهایت منجر به ایجاد آرایه های بزرگ بشه. برای مثال صدا زدن (0,1000000)range منجر به استفاده بالای 100 مگابایت از حافظه در حال استفاده خواهد شد.
به عنوان یک جایگزین بهینه تر ما تابع xrange رو پیاده سازی می کنیم. که فقط حافظه ای برای تکرار روی آبجکت و track کردن حالت جاری خواهد داشت که به نظر میرسه کمتر از 1 کیلوبایت است!
<?php function xrange($start, $limit, $step = 1) { if ($start < $limit) { for ($i = $start; $i <= $limit; $i += $step) { yield $i; } } else { for ($i = $start; $i >= $limit; $i += $step) { yield $i; } } }
جنریتور ها چطور کار میکنند ؟
جنریتورها از کلید واژه yield بجای return استفاده می کنند. Yield همانند return است و یک مقدار رو به تابع صدا زننده بر میگردونه ولی به جای حذف کردن فانکشن از stack ، وضعیت یا state آن را حفظ می کند. که این به فانکشن اجازه می دهد تا از جایی که دوباره صدا زده شده ادامه پیدا کنه.
جنریتور ها برای اجرای تکرارها روی مجموعه داده های بزرگ هم از لحاظ سرعت و هم از لحاظ میزان مصرف حافظه بسیار مناسب می باشند و از آنجا که نیازی ندارید تا کلاس Iterator رو extend کنید. برای پیاده سازی هم بسیار سریع تر می باشند.
زمانهایی وجود داره که ما می خواهیم حجم بزرگی از داده رو parse کنیم مثل فایلهای log یا باید محاسبات سنگین روی نتایج بزرگ دیتابیس ما ایجاد شوند. در این صورت ما نمی خواهیم که کل حافظه رو اشغال کنیم و فضایی برای کار دیگه نداشته باشیم! دیتا حتما نباید بزرگ باشه که generator ها تاثیر خودشون رو نشون بدهند! پیشنهاد می کنم همیشه از آنها استفاده کنید. فراموش نکنید هدف ما افزایش سرعت و استفاده کم از حافظه می باشد.
ء Return کردن کلیدها
زمانهایی وحود داره که به مقادیر بازگشتی ما باید به صورت key-value باشند در واقع ما به key های خودمون نیاز داریم. ما می تونیم key-value ها رو بدین صورت yield کنیم.
<?php function getRange ($max = 10) { for ($i = 1; $i < $max; $i++) { $value = $i * mt_rand(); yield $i => $value; } }
و می تونیم از آنها بدین صورت در تابع خودمون استفاده کنیم
<?php foreach (getRange(PHP_INT_MAX) as $range => $value) { echo "Dataset {$range} has {$value} value<br>" }
ارسال مقدار به generator ها
جنریتور ها مقدار هم می گیرند بدین معنی که ما میتوانیم به آنها یک مقداری را inject کنیم تا بدینوسیله به طور مثال به generator خودمون بگوییم که متوقف بشه یا خروجی رو تغییر بدهد، به طور مثال ما میخواهیم که تابع getRange بعد از دریافت مقدار ‘stop’ متوقف شود.
<?php function getRange ($max = 10) { for ($i = 1; $i < $max; $i++) { $injected = yield $i; if ($injected === 'stop') return; } }
مقدار را بدین صورت به آن ارسال می کنیم:
<?php $generator = getRange(PHP_INT_MAX); foreach ($generator as $range) { if ($range === 10000) { $generator->send('stop'); } echo "Dataset {$range} <br>" }
حالا که با generatorها آشنا شدید امیدوارم بیشتر از آنها استفاده کنید و بهینه تر کد بنویسید.