ویرگول
ورودثبت نام
سارا رضائی
سارا رضائیlinkedin.com/in/sara-rez
سارا رضائی
سارا رضائی
خواندن ۷ دقیقه·۴ سال پیش

طراحی برای پرفورمنس

سوال:

در طولِ فرآیند توسعه ی نرم افزار، چقدر باید به performance فکر کرد؟

اگر در هنگام توسعه ی نرم افزار، برنامه نویس بخواهد هر خط از کد را برای بیشترین سرعت، بهینه سازی کند، احتمالا سرعتِ توسعه بسیار کاهش خواهد یافت و پیچیدگی بالا می رود.

از سوی دیگر، اگر مواردِ مربوط به performance به طور کلی نادیده گرفته شود، در نهایت، سیستمی با ناکارآمدی زیاد وجود خواهد داشت، که احتمالا 5 تا 10 برابر، سرعت اجرای کمتری نسبت به آن چه که می توانست داشته باشد، دارد.

به جای انتخاب یکی از دو گزینه ی بالا، باید جایی در میانه قرار گرفت. بهترین رویکرد این است که، در هنگام توسعه ی نرم افزار، در انتخاب طراحی ها، آن هایی که به طور ذاتی، بهینه هستند و در عین حال، ساده و تمیز هستند را برگزینیم.

نکته ی اصلی، داشتنِ آگاهی درباره ی این موضوع است که، کدام عملیات، به طور اساسی، هزینه بر هستند.

مثال هایی از عملیات هزینه بر:

  • ارتباط شبکه ای

حتی درون یک دیتاسنتر، یک تبادل پیام، می تواند چیزی حدود 10 تا 15 میکرو ثانیه طول بکشد. حتما می دانید که این زمان، برابر با زمانِ مورد نیاز برای اجرای ده ها هزار دستور توسط پردازشگر است. در نظر داشته باشید که این رفت و برگشت ها، می توانند از 10 تا 100 میلی ثانیه (و نه میکرو ثانیه) طول بکشند.

  • ورودی و خروجی به دیسک

عملیات I/O دیسک، معمولا 5 تا 10 میلی ثانیه زمان می برد. این زمان برابر با اجرای میلیون ها دستور توسط پردازشگر است. البته تکنولوژی های جدید در زمینه ی حافظه، می تواند این زمان را تا 1 میکروثانیه کاهش دهد، ولی هنوز هم در مقایسه با سرعت اجرای دستور توسط پردازشگر، این زمان بسیار زیاد است.

  • تخصیص حافظه

وقتی از این دستور در زبان C استفاده می شود:

malloc(size);

و یا در ++C و Java یک شی ساخته می شود:

ClassName object = new ClassName();

اجرای این دستورات، معمولا شامل سربارِ قابل توجهی برای تخصیص، آزادسازی و GC است.

  • فقدان اطلاعات در cache

انتقال داده ها از DRAM به cache پردازشگر، به اندازه ی اجرای هزاران دستور، زمان می برد. در بسیاری از نرم افزار ها، برای اندازه گیری performance کلی، فقدان اطلاعات cache، به اندازه ی هزینه های محاسباتی، اهمیت دارد.

یک راهِ خوب برای یادگیری این که اجرای کدام عملیات بیشتر هزینه دارد، اجرا کردن micro-benchmark ها است، که نرم افزار های کوچکی هستند که هزینه ی اجرای یک عملیاتِ بخصوص را به صورت مجزا از سایر عملیات، اندازه می گیرند.

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

در بسیاری مواقع، یک رویکردِ بهینه، به سادگیِ یک روشِ کمتر بهینه است. مثلا این مورد را در نظر بگیرید:

برای ذخیره ی یک مجموعه ی بزرگ از object ها که قرار است توسط یک key، جستجو شوند، می توان از یک hash table یا یک ordered map استفاده کرد. هر دو در library های زبان Java قابل دسترس هستند و استفاده از هر دو به یک اندازه ساده است. با این حال، hash table حدود 5 تا 10 برابر سریع تر است. بنابراین در مواردِ شبیه به این، باید همیشه از hash table استفاده کنید مگر این که از ویژگی مرتب بودن ordered map استفاده کنید.

اگر بهینه نوشتنِ بخشی از کد، تنها از راهی امکان پذیر است، که باعث افزایش پیچیدگی می شود، برنامه نویس در یک دوراهی سخت قرار می گیرد. در این مواقع می توان اینگونه تصمیم گرفت: اگر طراحیِ بهینه تر، فقط مقدار کمی بر پیچیدگی سیستم می افزاید، و آن پیچیدگی، پنهان است (یعنی در واسط ها دیده نمی شود، بلکه فقط در پیاده سازی ها وجود دارد) می توان آن پیچیدگی را پذیرفت. ولی اگر طراحیِ بهینه، پیچیدگی را بسیار زیاد می کند، یا باعثِ ایجادِ واسط های پیچیده تر می شود، بهتر است، performance را فدا کرده و طراحی با پیچیدگی کمتر و performance پایین تر را انتخاب کرد و در آینده اگر performance تبدیل به یک مشکل شد، درباره ی بهینه کردن آن تصمیم گرفت. البته ممکن است با آگاهی از این که در یک موردِ بخصوص، اهمیت performance بسیار زیاد است، تصمیم گرفته شود که از ابتدا، طراحیِ بهینه تر انتخاب شود و بر پیچیدگی سیستم افزوده گردد.



فرض کنید با وجود این که در هنگام طراحیِ نرم افزار، به موارد performance دقت کرده و آن ها را لحاظ کرده اید، باز هم سیستم، عملکردِ لازم را ندارد. معمولا در این مواقع، اولین راه حلی که به ذهنِ افراد می رسد این است که بر اساس شهودِ خود، نسبت به این که کدام بخش از سیستم نیاز به اصلاح دارد، تصمیم می گیرند. این رویکرد بسیار اشتباه است! هیچ گاه نمی توان به شهودِ برنامه نویس، در مورد performance اعتماد کرد، حتی برنامه نویس های باتجربه. اگر شروع به انجامِ تغییرات بر اساس شهودِ خود کنید، احتمالا زمانِ زیادی روی مواردی که به بهبودِ عملکردِ سیستم کمک نمی کنند هزینه خواهد شد و در نهایت یک سیستم با پیچیدگیِ بیشتر و همان عملکرد قبلی (یا حتی بدتر) وجود خواهد داشت.

قبل از اعمالِ هر تغییری، رفتارِ فعلیِ سیستم را اندازه گیری کنید. اندازه گیری از دو جهت مفید خواهد بود:

1. جاهایی که بهبودِ آن ها تاثیری زیادی روی عملکردِ کلی سیستم می گذارد را مشخص می کند.

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



فرض کنید عملکرد بخش های مختلف را با دقت اندازه گیری کردید و بخش هایی از کد که آنقدر کند است که روی عملکرد کلی سیستم تاثیر محسوسی دارد شناسایی شد. در این مرحله، بهترین کار برای بهینه سازی سیستم، تغییرات ابتدایی است. مثلا استفاده از cache یا استفاده از یک الگوریتم بهتر (مثلا استفاده از BTree به جای list).

ولی گاهی اوقات، شرایط به گونه ای است که، یک تغییر ساده جوابگو نیست و نیاز به طراحی مجددِ بخش هایی از سیستم است. در این شرایط، ایده ی اصلی، پیدا کردن "مسیر بحرانی" و طراحی، پیرامونِ آن است.

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

با پاسخ دادن به این سوال شروع کنید:

کوچکترین مقدارِ کد، که برای انجامِ کل آن وظیفه باید اجرا شود، چیست؟

ساختارِ فعلیِ کد را به طور کامل فراموش کنید و در عوض، تصویر کنید که قرار است فقط آن "مسیر بحرانی" را با کمترین میزان کدنویسی، پیاده سازی کنید.

ممکن است کدِ فعلی، شرط های خاصی را چک کند؛ آن ها را کلا کنار بگذارید. ممکن است در کدِ فعلی، متدهای زیادی صدا زده شود، آن ها را هم کنار گذاشته و سعی کنید همه ی کد را در یک متد بگنجانید. همچنین احتمالا کدِ موجود، از متغیرها و ساختار داده هایی استفاده کرده است؛ در این صورت، باید فقط آن متغیرها و ساختار داده هایی که برای "مسیر بحرانی" ضروری هستند نگه دارید. حتی ممکن است چندین متغیر را در قالب یک متغیر ترکیب کنید.

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

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


خلاصه

کدهای پیچیده معمولاً کند هستند زیرا کارهای اضافی انجام می دهند. از سوی دیگر، اگر کدِ ساده و تمیزی بنویسید، احتمالاً سیستم شما به اندازه‌ای سریع خواهد بود که نیازی به نگرانی در مورد performance نداشته باشید. در موارد معدودی که نیاز به بهینه سازی performance دارید، نکته کلیدی دوباره سادگی است:

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



منبع

A philosophy of software design

توسعه نرم افزارطراحی نرم افزاربرنامه نویسینرم افزار
۱۰
۳
سارا رضائی
سارا رضائی
linkedin.com/in/sara-rez
شاید از این پست‌ها خوشتان بیاید