ترفندهایی برای کدنویسی سریع و کارآمد در متلب (MATLAB)

 

 

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

تمام موارد فهرست شده در زیر میتواند در نوشتن کد سریع کمک کند اما بیشترین تسریع در کد ناشی از 5 یا 6 روش اول است. اغلب این 6 روش بسیار ساده بوده و برای افراد مبتدی نیز قابل استفاده است. تنها مورد پیچیده، برداری‌کردن (vectorization) است که اگر از متلب زیاد استفاده میکنید، مهارت بسیار خوبی است که بهتر است آن را یاد بگیرید. موارد 10، 13 و 15 نیز میتواند اثر زیادی در شرایط خاص داشته باشد که البته به تجربه ثابت شده است که بهبودهای جزیی تا متوسط در پی دارند.

 

1- از profiler استفاده کنید: این ابزار زمان اجرای خط به خط برنامه را به عنوان خروجی برمیگرداند و به شما نشان میدهد که کدام خطوط برنامه نیاز به بهینه‌سازی دارند. البته همچنان که بهینه‌سازی کد مهم است ولی باید توجه داشت که برای کدی با زمان اجرای چندین ساعت چندان منطقی نیست که به منظور کم کردن چند ثانیه از آن ساعتها صرف بهینه‌سازی کنید.

 

 

2- در جای مناسب، مقداردهی اولیه را انجام دهید: (مثال: متغیری که در یک حلقه، در هر تکرار مقداری به آن اضافه میشود). هر زمان که لازم است مقداری به یک متغیر اضافه کنید (که مقداردهی اولیه نشده است)، متلب مجبور است که تمام متغیر را از ابتدا بر روی آدرس جدیدی از حافظه بنویسد که این مساله زمانبر است.

 

 

3- در جای مناسب از ماتریسهای تنک (sparse) استفاده کنید: در آرایه‌های فشرده که مقادیر صفر دارند، متلب همچنان باید محاسبات با مقادیر صفر را انجام دهد. با آرایه‌های تنک، از این محاسبات صرفنظر میشود زیرا متلب میداند که کجا مقدار صفر وجود دارد و خروجی معادل آن را نیز صفر فرض میکند. همچنین، باید روش استفاده از ماتریسهای تنک با دستور sparse را در صورت نیاز یاد بگیرید، در غیر این صورت، با دستور spalloc قبل از ساختن ماتریس تنک، مقداردهی اولیه را انجام دهید.

 

  1. مقداردهی اولیه در ماتریسهای تنک بد نیست. به طور مثال، در کدی که شامل حلقه برای حرکت بر روی آرایه تنک بزرگی است و در حدود 20 تا 30 دقیقه زمان میبرد، با اضافه کردن یک خط برای فراخوانی spalloc، این زمان به حدود 8 ثانیه کاهش می‌یابد.
  2. دسترسی به حافظه در جاییکه یک آرایه تنک را جزء به جزء میسازید، بسیار اهمیت دارد. استفاده از spalloc کمک میکند اما اگر این اجزاء به صورت مرتب و پشت سرهم اضافه نشوند، متلب مجبور است که هر بار که یک مقدار اضافه میشود، ترتیب آرایه را عوض کند. به لحاظ کاربردی، بدین معنی است که داده‌ها باید به صورت ستونی اضافه شوند و نه سطری. در آرایه‌های چندبعدی نیز ابتدا باید سمت چپ‌ترین بعد (dimension) آرایه تکمیل شود.

 

 

4- حلقه‌های for/while را در جای مناسب به فرم برداری درآورید: عملیات ماتریسی به طرز عجیبی در متلب سریع هستند اما حلقه‌ها نه چندان.

 

  1. در نسخه‌های اخیر، کامپایلر متلب بهبود یافته و برخی حلقه‌های ساده را به طور خودکار، به صورت برداری در می‌آورد. البته اگر آنها را خودتان برداری کنید، باز هم سریعتر اجرا میشوند. تفاوت (با توجه به پیچیدگی حلقه) نسبت به گذشته کمتر است اما همچنان میتوان گفت که حلقه‌های ساده‌ای که به طور خودکار توسط متلب، برداری میشوند حداقل 2 تا 3 برابر کندتر از نمونه‌های به فرم برداری درآمده به شکل صریح هستند.
  2. در جایی که به وارون ماتریس نیاز دارید، A\b سریعتر از inv(A)*b عمل میکند. از inv(A) جایی باید استفاده کرد که به خود مقدار inv(A) واقعا نیاز هست و یا مثلا جاییکه معکوس ماتریس به دفعات باید در مقدارهای مختلفی ضرب شود که خوب مقدار مجزای آن لازم است. در جاییکه که معکوس ماتریس وجود ندارد نیز باید از pinv(A) استفاده کرد.
  3. اگر حجم داده بالایی در اختیار دارید و با مشکل حافظه مواجه هستید، استفاده از حلقه برخی مواقع مزایای خود را دارد زیرا حلقه اغلب نسبت به عملیات برداری، حافظه کمتری مصرف میکند. هر چند که، همچنان میتوانید محاسبات داخل حلقه را برداری کنید که اثر استفاده از حلقه به جای محاسبات برداری را ناچیز کند.

 

 

5- استفاده از تابع bsxfun را یاد بگیرید: این روش به اندازه استفاده از عملیات ماتریسی موثر و کارآمد است.

 

  1. البته در نسخه‌های جدید متلب، عملیات ریاضی استاندارد به طور خودکار از این نوع بسط استفاده میکنند. اگر مطمئن هستید که نیاز به اجرای کد بر روی نسخه 2016a و قبل‌تر از آن ندارید، میتوانید از این تابع استفاده نکنید.
  2. توانایی عملکرد bsxfun با استفاده از توابع بی‌نام (تعریف شده توسط کاربر) قابل توسعه است اما لازم به ذکر است که bsxfun با تابع بی‌نام نسبت به حالتی که از توابع خود آن استفاده میکنید، بسیار کندتر اجرا میشود. البته ممکن است بر اساس اینکه چه کاری را میخواهید انجام دهید، این راه همچنان بهترین گزینه باشد. در یک تست انجام شده که ضرب دو بردار برای تولید یک ماتریس دوبعدی استفاده میشود، این نتایج حاصل شده است: bsxfun با تابع درونی خود @times بهترین بود، bsxfun با تابع بی‌نام کندتر بود اما همچنان بسیار سریعتر از پیاده‌سازی با حلقه دوتایی و اینکه تک حلقه با برداری کردن نیمی از محاسبات نیز کندتر از bsxfun با تابع خود و کمی سریعتر از bsxfun با تابع بی‌نام. البته این فقط یک مثال از این نوع است اما میتوان نتیجه گرفت که bsxfun با تابع بی‌نام نیز هنوز ممکن است در برخی موارد بهترین انتخاب باشد اما نه به صورت تضمین شده.

 

 

6- نحوه استفاده از اندیس‌گذاری منطقی را یاد بگیرید: تقریبا همیشه این روش سریعتر و به لحاظ حافظه، موثرتر از روشهای جایگزین است. در لینک (https://www.mathworks.com/help/stateflow/ug/supported-operations-on-chart-data.html) فهرستی از عملیات قابل استفاده در اندیس‌گذاری منطقی وجود دارد (مخصوصا دو قسمت اول)

 

  1. عملیات AND و OR مدارکوتاه (&& و ||) در گذاره‌های IF اولویت دارند ولی در بسیاری از موارد، استفاده از اندیس‌گذاری منطقی درست کار نمیکنند. به طور کلی، خارج از گذاره IF از & و | استفاده کنید. مثلا X(X>0 && X<2) درست کار نمیکند ولی X(X>0 & X<2) کار میکند.

 

 

7- پیشنهادهایی که تحلیلگر کد به طور خودکار به شما میدهد را نادیده نگیرید: برخی از آنها قابل صرفنظر هستند اما بیشتر آنها بنا به دلیل خاصی ظاهر میشوند.

 

 

8-در زمان مناسب از آرایه‌های عددی استفاده کنید: همچنین از آرایه‌های سلولی (cell) و ساختارها (structure). در استفاده از آرایه‌ای از ساختارها خیلی مراقب باشید زیرا همچنان که برای جداسازی داده‌ها مناسب هستند، جداسازی داده‌های عددی بین چندین ساختار در یک آرایه از ساختارها میتواند بسیار کندتر از یک ساختار با آرایه‌های عددی بزرگ به عنوان زیربخشهای آن، انجام شود. داده‌ای از آرایه‌های ساختاری مختلف لزوما به ترتیب در حافظه ذخیره نمیشوند و به صورت یک آرایه عددی استاندارد نمیتوان آنها را به صورت برداری محاسبه کرد. هرچند که، داده عددی در یک زیر بخش خاص از یک ساختار، (یا در یک عضو از آرایه سلولی) میتواند به صورت برداری محاسبه شود و به ترتیب در دسترس قرار گیرد (همچون آرایه عددی استاندارد) که این موضوع باعث میشود عملیات محاسباتی بسیار سریعتر انجام شود.

 

  1. برای نامگذاری پویای متغیرها از eval استفاده نکنید (از آرایه سلولی به جای آن استفاده کنید). واقعا میتوان چنین تصور کرد که تابع eval وجود ندارد.

 

 

9- حتی فکر استفاده از محاسبات سمبلیک را هم نکنید: مگر اینکه 1) با مساله‌ای مواجه هستید که بدون ریاضیات سمبلیک، مجبور میشوید که نتیجه را با محاسبات دستی و روی کاغذ بدست آورید. 2) به نسخه‌ای از نرم‌افزار Matehmatica دسترسی ندارید (که منصفانه است اگر بگوییم که محاسبات سمبلیک را خیلی بهتر از متلب انجام میدهد). متلب برای حل مسائل به صورت عددی طراحی شده است و نه به صورت سمبلیک. خیلی از افراد تازه‌کار ممکن است جذب محاسبات سمبلیک شوند چون درک آن ساده‌تر است.

 

 

10- تا حد امکان از توابع داخلی متلب استفاده کنید: توابع داخلی به عنوان بخشی از متلب ارائه شده است ولی هر تابعی که با نرم‌افزار متلب دریافت میکنید، توابع داخلی نیست و بسیاری از آنها فقط فایل .m ای هستند که برای کمک بیشتر به شما در نرم‌افزار قرار گرفته است. این پیشنهاد به این دلیل است که توابع داخلی از قبل کامپایل شده‌اند (کد عادی متلب در لحظه اجرا توسط کامپایلر JIT کامپایل میشود). تقریبا تمام توابع داخلی با همان سرعتی اجرا میشوند که تابع معادل نوشته شده به زبان C\C++\Fortran بهینه‌سازی شده اجرا خواهد شد. این خیلی غیرمعمول است که بتوانید کد متلب .m‌ای بنویسید که یک فرآیند مشخص را سریعتر از تابع داخلی معادل در متلب انجام دهد. غیرممکن نیست ولی خیلی به ندرت اتفاق خواهد افتاد.

 

  1. یک تفاوت توابع داخلی و موارد دیگر آن است که اگر فایل m. آنها را باز کنید(با دستور open یا edit) ملاحظه خواهید کرد که تابع داخلی تنها یک توضیح مختصر دارد و عبارت built-in function در انتهای آن دیده میشود ولی توابع دیگر به فرم استاندارد کد متلب هستند.

 

 

11- در جای مناسب از انواع داده منطقی (logical) و صحیح (integer) استفاده کنید: این کار به مصرف حافظه کمتر نیز کمک خواهد کرد. ممکن است این توصیه تاثیری به اندازه سایر موارد گفته شده در اینجا نداشته باشد ولی در برخی شرایط بسیار مفید است. متاسفانه، داده‌های عددی صحیح با آرایه‌های تنک کار نمیکنند ولی داده‌های منطقی قابل استفاده است.

 

  1. اندیس‌گذاری منطقی همیشه مقادیر منطقی تولید میکند و میتواند به سادگی یک آرایه عددی را به نوع منطقی تبدیل کند. مثال: یک آرایه از یک و صفر از نوع double را با دستور زیر به نوع منطقی میتوان تبدیل کرد:arrayLogical = (arrayNumeric == 1)

12- چیزهای مختلفی را امتحان کنید: حتی اگر مطمئن هستید که روش خاصی سریعترین روش ممکن است، چند روش را تست کنید. از اینکه کدام روش بهتر عمل میکند شگفت زده خواهید شد.

 

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

 

 

13- زمانیکه از حلقه استفاده میکنید، متغیر حلقه باید بر روی بیرونی‌ترین بعد سیگنال حرکت کند: اگر حلقه‌های تو در تو دارید، متغیر بیرونی‌ترین حلقه باید بیرونی‌ترین بعد آرایه باشد. تابع permute برای سازماندهی مجدد داده‌ها به شکل مناسب قبل از شروع حلقه قابل استفاده است.

 

  1. علت این موضوع نحوه سازماندهی حافظه توسط متلب است. هر بعد درونی‌تر داده، یک بلوک پیوسته از حافظه در بعد بیرونی بعدی را تشکیل میدهد. مثال: در یک آرایه 3بعدی (data(:,:,:))، حافظه به صورت زیر مرتب میشود:data(1,1,1),data(2,1,1),…,data(N1,1,1),data(1,2,1),…,data(N1,N2,1),data(1,1,2),…,data(N1,N2,N3)

    این بدان معنی است که اگر بر روی صفحه‌های 2بعدی به صورت data(:,:,nn) حرکت کنید، همیشه از یک بخش حافظه به صورت ترتیبی و پشت سرهم استفاده میکنید ولی در حالتهای data(nn,:,:) و data(:,nn,:) داده‌ها در فضای حافظه نامرتب بوده و به صورت پخش شده در فضا هستند. برای حلقه تو در تو (n1 از حلقه بیرونی و n2 از حلقه درونی) صدا زدن data(:,n2,n1) بدان معنی است که داده با همان ترتیبی صدا زده میشود که بر روی دیسک ذخیره شده است و هر بار صدا زدن داده ها معادل برداشت یک بلوک پیوسته از حافظه است. هر ساختار دیگری برای نوشتن حلقه، منجر به پخش شدن بازخوانی داده در حافظه میشود.مطلب بالا با فرض حرکت حلقه بر روی آرایه عددی است و لزوما به موارد شامل سلولها و ساختارها قابل تعمیم نیست. به طور کلی، اگر حلقه‌های تو در تو با آرایه‌های از نوع سلول یا ساختار دارید، بهتر است که اولویت را به پایان دادن حلقه‌های روی آرایه عددی بدهید (این آرایه میتواند عناصر موجود در سلول یا ساختار نیز باشد). البته مطمئن نیستم که توالی مشخصی برای حرکت حلقه روی عناصر مختلف یک سلول یا زیربخشهای یک ساختار وجود داشته باشد. به نظر میرسد همه این بخشها از قبل در حافظه جداسازی شده باشند و به همین دلیل مرتب سازی تاثیر کمی خواهد داشت. همچنین این بدان معنی است که آنها با عملیات برداری به همان خوبی که آرایه‌های عددی کار میکنند، عمل نخواهند کرد. حافظه غیرترتیبی و عدم توانایی بهره‌گیری کافی از عملیات برداری دو دلیل عمده‌ای است که ترجیح ما برای استفاده هر چه بیشتر از آرایه‌های عددی را توجیه میکند (مورد 8 همین مقاله).

     

  2. یک مثال حلقه تو در تو برای حرکت صفحات 2بعدی از یک آرایه 5بعدی میتواند به صورت زیر باشد:

    dataOUT=zeros(N1,N2,N3,N4,N5);      % pre-allocate. data has 5 dimensions

    % dataIN is the same size as dataOUT

    for n1=1:N5        % outer loop uses the LAST dimension.

        for n2=1:N4 % middle loop uses the 2nd to last dimension.

            for n3=1:N3   % innermost loop uses the 3rd to last dimension.

                dataOUT(:,:,n3,n2,n1) = foo( dataIN(:,:,n3,n2,n1) );             

            end

        end

    end

     

14- در زمان کدنویسی حتما برای هر قسمت توضیح (comment) بنویسید به خصوص زمانیکه در آینده میخواهید از کد مجددا استفاده کنید: این موضوع مثال دیگری از کاهش زمان کدنویسی است که از اجرای آن نشات نمیگیرد بلکه مربوط به زمانی میشود که میخواهید از کد استفاده کنید یا آن را اصلاح کنید و یا اینکه بخشی را به آن اضافه کنید. اگر جزء افراد خاصی که همه چیز را با تمام جزییات میتوانند به یاد بیاورند نباشید به احتمال زیاد بعدا کاری را که هر بخش از کد نوشته شده توسط شما، انجام میدهد را به خاطر نخواهید آورد. با صرف دقایقی برای نوشتن توضیح کد، ساعات زیادی را در آینده برای سر در آوردن از آنچه که انجام داده‌اید، صرفه‌جویی خواهید کرد. این موضوع برای افراد دیگری نیز که میخواهند از کد شما استفاده کنند، مفید است.

 

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

 

 

15- اگر برای بهینه سازی چیزی ایده یا راه حلی ندارید از تابع mex. استفاده کنید: توابع mex. کدهای Cایی است که از قبل کامپایل شده و به فرمتی درآمده است که توسط متلب قابل استفاده باشد. نسبت به توابع خود متلب، این توابع سرآیند یا overhead بیشتری دارد ولی خود تابع نسبت به نمونه m. آن بسیار سریعتر اجرا میشود.

 

  1. بخش matlab file exchange (FEX) تعدادی توابع mex. رایگان دارد. کامپایل کردن توابع mex. برخی اوقات میتواند نکاتی ریزی همراه خود داشته باشد هر چند که اگر تنظیم کامپایلر در متلب به درستی انجام شود، آسان خواهد بود و بعد از کامپایل کردن، استفاده از این توابع مشابه توابع m. متلب است.
  2. پس از تغییر کامپیوتر، سیستم عامل آن و یا تغییر سخت‌افزاری ( مثلا به روزرسانی cpu یا برد اصلی) هر تابع mex. باید مجددا کامپایل شود. البته بدون کامپایل مجدد نیز توابع قابل استفاده است ولی ممکن است که: 1- با راندمان پایینتری کار کند. 2- اصلا کار نکند. 3- کار کند ولی به طور نامحسوس نتایج غلط تحویل شما دهد.

 

 

اگر بخواهم نکات مهم و اصلی را به صورت خلاصه بیان کنم، باید بگویم که:

 

1- از profiler برای مشخص کردن بخشهای اصلی زمانبر در کد خود که نیاز به بهینه‌سازی دارد، استفاده کنید.

 

2- تا جاییکه ممکن است عملیات زمانبر و سنگین را به کمک روشهای ماتریسی یا توابعی همچون bsxfun برداری‌سازی کنید. اگر این روش مناسب نیست، از تابع کامپایل شده دیگری که سریعتر است استفاده کنید. اگر داده شما تعداد زیادی صفر دارد، از آرایه‌های تنک استفاده کنید. (که توسط sparse یا spalloc تولید میشوند).

 

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

        

 

 

 

منبع: https://www.reddit.com