امروزه شرکت های تکنولوژی در رقابتی شدید برای افزایش بهرهوری و عملکرد محصولات و خدماتشان هستند تا بتوانند، عطش بی پایان کاربران را هر چه بهتر پاسخ دهند و سهم بیشتری از بازار را در اختیار بگیرند. در همین راستا شرکت ها سعی میکنند تجربه ی کاربری روان تر و بهتری برای کاربرانشان فراهم کنند. تجربه ی کاربری روان تر، توان پردازشی بیشتری هم طلب میکند و شرکت های تولید کننده ی پردازنده برای پاسخ به همین نیاز اقدام به سرمایه گذاری و توسعه ی هرچه بیشتر روش هایی برای ساخت ترانزیستور و پردازنده هایی با عملکرد و بازده بهتر کرده اند. بخشی از نیازِ فرایند بهبود تجربه ی کاربری، به وسیله ی سخت افزار قوی تر تامین میشود و بخشی هم از طریق افزایش بهره وری و بهینه سازی نرم افزار هایی که روی سخت افزار مذکور اجرا میشوند، انجام میپذیرد. از دهه ی 80 میلادی به بعد که کامپیوتر های شخصی و سپس تلفن های همراه به صورت گسترده مورد استفاده قرار گرفتند، بحث مربوط به افزایش سرعت انجام وظایف و سریع تر و قوی تر بودن محصولات بین شرکت های بزرگ بالا گرفت. در همه ی این سال ها، کاربران خواستار دستگاه هایی سریع تر و بهینه تر از قبل بودند، اما فرایند توسعه ی تکنولوژی جدید تر و بهینه تر برای شرکت ها هزینه ی زیادی داشته و دارد و هر چه جلوتر میرویم به علت پیچیده تر شدن تکنولوژی های ساخت، هزینه ها بالاتر میرود، به همین دلیل شتاب ساخت و توسعه کامپیوتر های سریع تر در دهه ی اخیر کاهش یافته است. به صورتی که شرکت اینتل حدود 3 سال روی تکنولوژی ساخت 14 نانومتری متوقف شده بود و به تازگی امسال تکنولوژی ساخت 10 نانومتری را معرفی کرد. علی رغم توقف شرکت های سازنده سخت افزار برای ساخت پردازنده های سریع تر، شرکت های بزرگ فناوری مثل اپل، سامسونگ و هواوی و ... باید برای پاسخ گویی به نیاز های کابرانشان راهی پیدا میکردند. پاسخ این شرکت ها بهینه سازی نرم افزار ها بود. وقتی نمیتوانیم دستگاه های قوی تر بسازیم، میتوانیم برنامه ها، اپلیکیشن ها، سیستم عامل ها ی بهینه تر توسعه دهیم و با همان سخت افزار قبلی، برنامه ها را تا حد خوبی روانتر و سریعتر اجرا کنیم که بتوانیم تا حدی خلا نبود سخت افزار های جدید را پر کنیم. در همین راستا در چند سال اخیر بحث مربوط به کدنویسی بهینه و الگوریتم های بهبود عملکرد نرم افزار ها به شدت بالا گرفته و مهندسان و دانشمندان کامپیوتر وقت و هزینه ی زیادی را برای توسعه ی این الگوریتم ها صرف کرده اند.
در این مقاله به بررسی فاکتور های بهینه بودن کد و معرفی تعدادی از پر تکرارترین موارد برای بهبود سرعت اجرای کد های شما میپردازیم.
کد بهینه چیست؟
در علم کامپیوتر، بهینه سازی نرم افزار یا بهینه سازی کد به فرآیندی گفته میشود که با تغییر بخش های مختلف نرم افزار، کاری کنیم که نرم افزار ما بهینه تر و موثر تر کار کند یا به عبارتی منابع سیستمی کمتری مصرف کند. مثلا اگر کاری کنیم که نرم افزارمان Ram کمتری مصرف کند یا در مدت زمان کمتری تعداد درخواست های بیشتری را پاسخ دهد، میگوییم نرم افزارمان را بهینه تر کرده ایم.
به طور کلی میتوان فاکتور های متنوعی را برای سنجش بهینگی کدمان معرفی کنیم. افزایش بهینگی کد میتواند حاصل مواردی مانند، افزایش کیفیت کد، بهبود اثرگذاری کد، حجم کمتر، مصرف کمتر Ram و اجرا شدن سریع تر برنامه مان باشد. نکته مهمی که باید در اینجا اشاره کنیم این است که، تحت هیچ شرایطی پس از بهینه سازی کد، خروجی کد نباید تغییری بکند. به عبارتی بحث بهینه سازی کد نباید خللی در نحوه عملکرد کد ایجاد کند. تغییر در خروجی کد بهینه شده فقط در شرایطی برای ما قابل قبول است که عملکرد سیستم به قدری بهبود پیدا کند که بر ثابت نگه داشتن خروجی کد اولویت پیدا کند.
بهینه سازی میتواند هم توسط انسان (برنامه نویس) و هم توسط کامپیوتر انجام شود. امروزه کامپایلر ها بخش بزرگی از بهینه سازی هایی که در گذشته انسان انجام میداد را انجام میدهد. همین طور نرم افزار هایی وجود دارند که با تحلیل و بررسی کد ما، میتواند مشکلات احتمالی کدمان را به ما گوش زد کند یا آنها را بر طرف کند.
به طور کلی بسته به نوع بهینه سازی و اینکه بهینه سازی توسط چه کسی انجام میشود میتوان، بهینه سازی را به دو دسته High-level و low-level دسته بندی کرد. در سطح high-level که معمولا توسط برنامه نویس انجام میگیرد، اکثرا بهینه سازی در سطح متد ها، فرایند های نرم افزاری، الگوریتم های پیاده سازی شده، کلاس ها و بررسی و جایگزینی loop ها انجام میگیرد. برنامه نویس ها در سطح high-level میتوانند، با بررسی طراحی کل سیستم، با اعمال تغییراتی در سطوح بالای طراحی و پیگیری آن در تمام بخش ها، تغییرات چشم گیری را در بازده و عملکرد سیستم ایجاد کنند. بهینه سازی سطح دوم یا low-level زمانی انجام میگیرد که source code ها در حال ترجمه به زبان ماشین هستند. در این سطح معمولا بهینه ساز های اتوماتیک یا کامپایلرها عملیات بهینه سازی را انجام میدهند، ولی این نرم افزار ها هر چقدر هم که خوب و با دقت کار کنند، باز هم در حد یک برنامه نویس حرفه ای، نمیتوانند بهینه سازی کنند.
به طور معمول طبق آمار به دست آمده حدود 90 درصد از زمان اجرای نرم افزار برای اجرای 10 درصد کد های آن صرف میشود. به همین دلیل بهتر است که به جای اینکه وقت تان را برای بهینه سازی کل کد بگذارید، آن 10 درصد را بهینه تر کنید و نتایج به مراتب بهتری بگیرید.
گاهی اوقات کدی که بهینه کرده ایم، در زمان کمتری دستورات بیشتری را اجرا میکند ولی از طرفی انرژی بیشتری هم مصرف میکند و ما باید با توجه به نیاز ها و محدودیت هایمان به یک توازن مناسب بین خوانایی، بهینه بودن و محدودیت منابعمان دست پیدا کنیم و شاید همین نکته تفاوت یک برنامه نویس زبده و کاربلد، با یک برنامه نویس تازه کار باشد.
مواردی در باب بهینه سازی
1 – از طراحی سیستم تان شروع کنید.
مهم ترین عاملی که میتواند در افزایش بهره وری سیستم شما اثر گذار باشد، بهبود طراحی سطح بالای سیستم است. اگر در برنامه تان از توابعی با بهترین عملکرد و کمترین زمان اجرا استفاده کنید اما طراحی کلی سیستم بهینه نباشد، فایده ای ندارد و نمیتوانید از تمام پتانسیل موجود در اختیارتان استفاده کنید. پس بهتر است که بهینه سازی را از بخشها و ماژول های سطح بالای سیستم تان شروع کنید و با اعمال این تغییرات تا سطوح پایین پیش بروید.
2 – الگوریتم ها میتوانند پرنده نجات شما باشند.
پس از بهینه سازی طراحی سیستم تان که شِما و طرح کلی برنامه را مشخص میکند، میتوانید برای پیاده سازی هر بخش، از الگوریتم های مناسب استفاده کنید و تاثیر طراحی خوب را دو چندان کنید. طراحی خوب مانند یک چاقو است و الگوریتم های خوب مانند لبه ی تیز برای آن چاقو. چاقویی که لبه ی تیزی برای بریدن نداشته باشد، نمیتواند وظیفه ی اصلیش که بریدن است را به درستی انجام دهد.
3 – loop ها میتوانند بلای جان شما شوند.
به طور کلی لوپ ها جاهایی هستند که برنامه شما پتانسیل بیشترین هدر رفت زمان را دارد. حال اگر داخل لوپ شما، کدی نوشته باشید که نیاز نیست حتما داخل لوپ قرار بگیرد، افتضاح به بار میآید. به کد زیر توجه کنید:
$x = 10;
$y = 10;
for($i = 0; $i < 20: $i++) {
$z = $x + $y;
//do sth else
}
در کد بالا همانطور که مشاهده میکنید، ما در هر بار اجرای عبارت داخل for یک بار هم عملیات مقدار دهی به متغیر z را انجام میدهیم. پس ما داریم عملیات مقدار دهی را 20 مرتبه انجام می دهیم بدون اینکه نیاز داشته باشیم، حال فکر کنید به جای مقدار دهی، عملیات های زمان بر دیگری مانند ذخیره در فایل یا اتصال و دریافت اطلاعات از دیتابیس را قرار میدادیم، به مراتب زمان اجرای برنامه ما بیشتر میشد. به همین دلیل "حتما" و "فقط" کارهایی را داخل لوپ انجام دهید که باید داخل لوپ انجام شوند و تا جایی که میتوانید حجم کد داخل لوپ را کم کنید.
4 – حواستان به شرط های loop ها باشد.
تکه کد زیر را در نظر بگیری:
for($i = 0; $i < count($arr) ; $i++) {
//do sth else
}
عملیات شمردن تعداد المان های آرایه ی arr در هر بار اجرای این loop تکرار میشود. حال فرض کنید که این آرایه شامل دیتایی حجیم از دیتابیس باشد، مثلا 20000 داده و به تعداد بیست هزار بار این آرایه شمرده میشود تا وقتی که loop به انتها برسد. میتوان با کد زیر این مشکل را حل کرد:
$arr_length = count($arr);
for($i = 0; $i < $arr_length ; $i++) {
//do sth else
}
و بدین ترتیب، فقط یکبار طول این آرایه را میشماریم.
5 – کد های مرده را از برنامه تان حذف کنید.
کد مرده به اصطلاح به کدی گفته میشود که هیچ وقت اجرا نمیشود. به مثال زیر توجه کنید:
public function store($data)
{
// store data
return $object;
//do sth
}
در مثال بالا در متد store ابتدا داده های ورودی را ذخیره میکنیم و بعد object ساخته شده را باز میگرداینم. ولی تکه کد بعد از return هیچ گاه و تحت هیچ شرایطی اجرا نمیشود. این کد را کد مرده میگویند. مشکل کد مرده این است که از طرفی باعث افزایش حجم نرم افزار ما میشود، همچنین مدت زمان کامپایل شدن نرم برنامه توسط کامپایلر را افزایش میدهد و همه این مشکلات به خاطر کدی است که هیچ گاه اجرا نمیشود. پس حتما توجه داشته باشید که کد های مرده را از برنامهتان حذف کنید.
6 – ضرب، هزینه بر و زمان بر است، لطفا از جمع استفاده کنید.
این مورد نکته ی نرم افزاری ندارد بلکه از دل کامپیوتر و پردازنده شما میآید. مدار محاسبه ی ضرب به مراتب بزرگ تر و گران تر از مدار محاسبه ی جمع است و به همین دلیل تعداد واحدهای جمع کننده در مدارها و پردازنده های کامپیوتر به مراتب بیشتر از تعداد مدارات ضرب کننده است. به همین دلیل اگر در جایی از کدتان میخواهید متغیر a را در 2 ضرب کنید بهتر است به جای اینکه بنویسید a * 2
، بنویسید a + a
، بدین ترتیب عملیات مد نظر شما سریع تر و ارزان تر انجام میشود. شاید در نگاه اول این نکته نظرتان را جلب نکند ولی اگر به تعداد ضرب هایی که در یک پروژه نرم افزاری انجام میدهید توجه کنید، به تغییراتی که همین نکته کوچک ولی پر کاربرد اعمال خواهد کرد پی میبرید. البته امروزه اکثر کامپایلر ها چنین جایگزینی را به صورت اتوماتیک در هنگام کامپایل کردن، انجام میدهند.
7 – تا جای ممکن دستورات و عملیات های تکراری را حذف کنید.
به کد زیر توجه کنید:
$a1 = $b * $c + $e;
$a2 = $b * $c + $f;
در کد بالا ما برای محاسبه ی مقدار a1 و a2، عملیات گران و زمان بر ضرب دو متغیر b و c را دوبار انجام میدهیم. ممکن بود این عملیات تکراری خواندن داده ای از دیتابیس باشد. بهتر است که این عملیات یک بار انجام شود، و بدین ترتیب در زمان و لود کاری روی cpu صرفه جویی کنیم. کد جایگزین به صورت زیر خواهد بود:
$tmp = $b * $c;
$a1 = $tmp + $e;
$a2 = $tmp + $f;
8 – به جای متغیر constant از خود مقادیر استفاده کنید.
در گذشته مقادیر constant
در حافظه ذخیره میشدند و در هر جای برنامه که نیاز داشتیم، صدا زده میشدند. این رویه باعث اشغال فضای اضافی در Ram میشد، برای حل این مشکل و همچنین جلوگیری از کم شدن خوانایی و تمیز بودن کد، برنامه نویسان، کامپایلر ها را طوری توسعه دادند که در هنگام کامپایل کردن، این متغیر های constant
را با مقادیر ثابت جایگزین کند. پس در این مورد شما نیاز به انجام کاری ندارید. بدین ترتیب هم خوانایی و تمیز بودن کد (که در مقاله ی قبل به آن اشاره کردیم) رعایت میشود، همچنین فضای Ram بیشتری دارید و همه این ها به لطف کامپایلر های قوی تر و هوشمند تر است😊.
9 – حواستان به garbage collector باشد.
در طول اجرای کد ها متغیر های مختلفی روی حافظه Ram ذخیره میشوند و اگر بعد از اتمام اجرای کد، حافظه ی رزرو شده آزاد نشود، میتواند باعث بروز خطا در سیستم میزبان شده و نتایج غیرمطلوبی را به بار آورد. در همین راستا اکثر زبان های برنامه نویسی از ابزاری به نامGarbage Collector استفاده میکنند که بدین صورت کار میکند: بعد از اتمام اجرای برنامه یا حتی در حین اجرای برنامه، داده هایی که بدون استفاده قرار میگیرند، از حافظه پاک شده و آدرس های حافظه در دسترس قرار میگیرند تا داده های دیگری را در خود ذخیره کنند. این عملیات یافتن و حذف داده ی بلا استفاده، خود نیاز به مصرف مقداری از منابع سیستم دارد به همین منظور بهتر است که در حد توانایی و دانش، برنامه نویس بعد از استفاده از متغیر ها، آن ها را از حافظه حذف کند تا کار بخش garbage collector که به صورت اتوماتیک این کار را بر عهده دارد، کمتر شود. البته نباید بدون داشتن اطلاعات و دانش دست به چنین کاری زد چون که ممکن است عملکرد برنامه مان مختل شود.
10 – تا جایی که میتوانید از توابع پیش فرض زبان برنامه نویسی تان استفاده کنید.
زبان های برنامه نویسی خود مجموعه ی بزرگی از توابع و متد ها را در اختیار کاربر قرار میدهند. همچنین باید توجه داشت که تقریبا در همه ی موارد عملکرد توابع پیش فرض خود زبان برنامه نویسی به مراتب سریع تر از تابعی است که ما مینویسیم، به همین دلیل بهتر است به جای اختراع دوباره ی چرخ، با تشکر از مهندسان مخترع چرخ، از چرخ آنها در برنامه ی خودتان استفاده کنید.
11 – تا زمانی که مجبور نشدهاید از ارث بری استفاده نکنید.
ایجاد کلاس جدید و ارتباط بین کلاس های مختلف از طرفی هم باعث ایجاد وابستگی بین بخش های مختلف برنامه می شود که البته گاهی نیاز است و مجبوریم چنین کاری انجام دهیم.
توجه کنید، که منظور ما این نیست که برنامه نویسی شئ گرا را کنار بگذارید، نه، مقصود ما این است که تعداد object ها و کلاس های ساخته شده در برنامه تان را مدیریت کنید، ارث بری در واقع به صورت پنهان، حافظه ی Ram بیشتری از شما اشغال میکند، چون که با ساخت object از کلاس فرزند، علاوه بر کد های کلاس فرزند، کد های کلاس پدر هم در object ساخته شده وجود دارند، که باعث افزایش حجم object ساخته شده میشود و ما چنین چیزی نمیخواهیم. پس هر زمان که میخواستید رابطه ی ارث بری بین دو کلاس ایجاد کنید، این نکته را به خاطر داشته باشید، در آینده به بایت بایت حافظه ی آزاد شده نیاز پیدا خواهید کرد😉.
12 – اگر با دیتابیس کار میکنید، همین الان، سیستم کش(Cache) برنامه تان را راه بیاندازید.
اتصال به دیتابیس و دریافت و ارسال داده به آن از زمانبرترین عملیات هایی است که هر برنامه انجام میدهد، پس سعی کنید اطلاعات دریافتی از دیتابیس را کش کنید که هر چه کمتر به دیتابیس نیاز داشته باشید. این موضوع زمانی اهمیت بیشتری پیدا میکند که تعداد درخواست ها به سرور یا برنامه ی شما افزایش یابد و در نتیجه شما نیاز به ارسال درخواست های زیادی به دیتابیس داشته باشید، در چنین زمان هایی کش کردن درخواست ها میتواند به طرز اعجاب انگیزی، از وارد شدن فشار زیاد به دیتابیس برنامهتان جلوگیری کند.
13 – استفاده از متغیر های global را محدود کنید.
متغیر های global در تمام طول اجرای برنامه در حافظه باقی میمانند و garbage collector زبان ها هم آن را پاک نمیکنند، پس بهتر است در مواردی که نیازی به تعریف متغیر گلوبال ندارید، آن ها را local تعریف کنید.
14 – متد های با عملکرد سریع تر را در زبان برنامه نویسی خود بیابید.
در هر زبان برنامه نویسی متد هایی با عملکرد نسبتا مشابه یافت میشود. اما وجه تمایز اصلی این متد ها سرعت اجرای آن ها است، بهتر است متناسب با زبان برنامه نویسی خود، این متد های مشابه را بیابید و از متد های سریع تر استفاده کنید. خیلی از این متد ها در طول نسخه های مختلف زبان توسعه داده شده اند ولی متد های قبلی برای پشتیبانی بیشتر همچنان در دسترس هستند، پس بهتر است که از متد های جدید تر و سریع تر استفاده کنید.
15 – به جای استفاده از عملگر ==، از عملگر === استفاده کنید.
در هر موقعیتی که مقصود ما از مقایسه هم از نظر نوع و هم از نظر مقدار است بهتر است از عملگر ===
استفاده کنید، چرا که عملگر ==
قبل از مقایسه ی دو مقدار، اگر از یک جنس نباشند سعی میکند که این دو را به یک جنس تبدیل کند که این عملیات، هم به منابع حافظه و پردازنده نیاز دارد و اگر نیازی به این تبدیل فرمت ها ندارید، بهتر است فشاری روی منابع سرور وارد نکنید.
توجه کنید که در زبان پایتون ما عملگر ===
را نداریم و به جای آن باید به صورت جدا type متغیر ها را مقایسه کنید، اما باز هم مفهوم مقایسه ی مقدار به همراه نوع برقرار است و بهتر است که هر دو را مقایسه کنید.
16 – برای برنامه خود تست بنویسید.
تست ها از دو جهت به کمک شما میآیند، یک اینکه بعد از بهینه سازی برنامه تان میتوانید با اجرای این تست ها اطمینان حاصل کنید که برنامه تان هنوز هم مانند قبل کار میکند و تغییرات اشتباهی در آن نداده اید، دو اینکه میتوانید مدت زمان اجرای تست و در نتیجه تاثیر بهینه سازی تان را در اجرای برنامه ببینید.
17 – عملگر & را با && جایگزین کنید.
وقتی بین دو شرط عملگر &&
میگذارید، اگر شرط اول اشتباه باشد، شرط دوم چک نمیشود ولی اگر از عملگر &
استفاده کنید، هر دو شرط چک میشوند و سپس با هم Bitwise AND شده و در نهایت نتیجه را به شما باز میگرداند. مثلا اگر در یکی از این شرط ها شما یک درخواست به دیتابیس بفرستید، اگر شرط اول false
شود در صورتی که از عملگر &&
استفاده کرده باشید دیگر شرطی که نیاز به ارتباط با دیتابیس داشت، چک نمیشود. حال فرض کنید این دو شرط باید برای تک تک request های برنامه تان چک شوند. در این صورت شما بار زیادی را از دوش دیتابیس برداشته اید، فقط با توجه به استفاده از &&
به جای &
.
18 – مراقب توابع بازگشتی باشید.
توابع بازگشتی در حین اینکه میتوانند بسیار کاربردی باشند و مسائل را برای ما راحت تر حل کنند، میتوانند خیلی سریع به گلوگاه برنامه ما تبدیل شوند. بهتر است در هنگام نوشتن توابع بازگشتی، این موارد را در نظر داشته باشید و با توجه به امکان گلوگاه شدن این توابع، اقدام به توسعه ی آن ها نمایید. مثلا میتوانید نتیجه ی این مقادیر را کش کنید یا اینکه بعضی از مقادیر را در حافظه ذخیره کنید و به جای اینکه از ابتدا شروع به محاسبه ی مقدار نهایی کنید، از آن مقادیر ذخیره شده استفاده کنید و تعداد مراحل مورد نیاز برای حل مسئله را کاهش دهید.
جمع بندی
در این مقاله سعی شد که بدون توجه به زبان برنامه نویسی خاصی، نکاتی که در همه زبان ها میتواند عملکرد برنامه شما را تحت تاثیر قرار دهد، مطرح شود. میتوانید با جست و جو در گوگل نکاتی که مخصوص زبان خودتان هست را بیابید و علاوه بر موارد بالا از آن ها هم استفاده کنید و نتایج به مراتب بهتری هم دریافت کنید.
به طور کلی بحث بهینه سازی برنامه ها باید خیلی جدی تر از قبل توسط برنامه نویسان به خصوص برنامه نویس های تازه کار دنبال شود، چرا که در دنیای امروز که هر برنامه در لحظه باید به صدها یا شاید هزاران درخواست پاسخ دهد، 0.1 ثانیه ها ارزشمند میشوند و همین زمان های کوتاه میتواند باعث شود کاربران، سایت یا اپلیکیشنی را بر دیگری ترجیح دهند.
منابع
1. https://www.viva64.com
2. https://www.tutorialspoint.com
3. https://www.geeksforgeeks.org
4. https://en.wikipedia.org
5. https://www.toptal.com
6. https://www.sciencedirect.com
7. https://medium.com
8. https://www.gatevidyalay.com
9. http://wwwx.cs.unc.edu