مدتی بود که چند توییت فنی در مورد جزییات مدیریت حافظه موتور بازی سازی Unity نوشتم که مورد استقبال دوستان قرار گرفت و به پیشنهاد همین دوستان تصمیم گرفتم همان مطالب را با جزییات بیشتر اینجا منتشر کنم، این اولین تجربه من با ویرگول هست، امیدوارم مفید باشه.
موتور Unity برای زیرسیستمهای مختلف خود حافظه را به روشهای مختلفی مدیریت میکند، الگوریتمها و روشهای مدیریت حافظهی درونی زیر سیستمها از دسترس و چشم کاربران موتور دور و مخفی هستند برای همین اطلاع زیادی از آنها نداریم، اما بخشی که بیشتر با آن درگیر هستیم و کمی با نحوهی عملکرد آن آشنا هستیم بخش scripting runtime است که میتواند Mono یا IL2CPP باشد. بنابراین این مقاله روی این بخش از حافظهی موتور یونیتی متمرکز خواهد بود.
به طور کلی قسمتی از حافظه که برای تخصیص پویای حافظه استفاده میشود Heap میگویند. حافظهی Heap مدیریت شده، حافظهای است که برای ذخیرهی همهی اشیاء اسکریپتها استفاده میشود. به این علت مدیریت شده نامیده شده که زمانی که یک شی هیچ ارجاعی نداشته باشد توسط زبالهروب (garbage collector) بازیابی میشود.
زبالهروب حافظهی یونیتی که کار مدیریت حافظه را به عهده دارد الگوریتم Boehm را پیداه سازی کرده و دارای ویژگی هایی است که دانستن آنها به نحوهی استفادهی صحیح ما از یونیتی کمک میکند.
این الگوریتم از نوع الگوریتمهای mark-sweep است، که دارای دو مرحله اصلی است، مرحله اول شامل پیمایش حافظه برای پیدا کردن اشیاء قابل دسترسی و علامت زدن آنها (mark)، و مرحله دوم که حافظههای غیر قابل درسترسی پاک میشوند(sweep).
زباله روب حافظه در یونیتی به نوعی stop-the-world garbage collector است، به این معنا که تمام فرآیندهای موتور باید متوقف شود تا زبالهروب کار خود را انجام دهد و این موضوع برای بازیهایی که میخواهند frame rate نرم و روان داشته باشند باعث دردسر خواهد بود.
این زباله روب از نوع Non-generational است، بدین معنا که برای پیدا کردن حافظههایی که زباله محسوب شده و باید جمع آوری شوند باید کل حافظهی Heap را پیمایش کند. در مدلهای generational حافظه به چند بخش (generation) تقسیم شده و هر بخش با فرکانس متفاوت بررسی میشود.
بعد از مرحلهی پاکسازی در اکثر garbage collector ها، در حافظه فضاهای خالیای به وجود میآید که با compact کردن و shift دادن آن به یک سمت از بین میروند. ولی مدیریت حافظه در یونیتی از نوع
non-compacting است بنابراین حافظهی مدیریت شده در یونیتی به شدت مستعد fragment شدن است. علت این امر هزینهی زمان اجرای بالای جابه جایی حافظه است که این کار را برای یک موتور بازی سازی نامناسب میکند.
وقتی حافظهی جدیدی درخواست میکنید gc ابتدا در بین فضاهای خالی موجود دنبال یک مکان خالی به اندازهی درخواست شده میگردد، اگر جای خالی پیدا شود آن را به شئ شما اختصاص میدهد در غیر این صورت مجبور به گسترش حافظه است که بسته به سیستم عامل و دستگاه مشخص میشود ولی در اکثر سیستم عاملها یونیتی حافظه را دوبرابر میکند.
این حافظه هیچگاه به سیستم بازگردانده نمیشود به عبارت دیگر Heap مدیریت شده فقط میتواند رشد کند.
به طور کلی استفاده از حافظهی پویا و رها سازی آن و اجرای garbage collector باعث متفرق شدن حافظه میشود، بنابراین باید تا جای ممکن از حافظهی پویا استفاده نکرد و حافظههای مورد نیاز را ابتدا تخصیص دهیم و سعی کنیم از آنها مجدد استفاده کنیم.
هچنین یکی از مهمترین عواملی که باعث تخصیص حافظهی پویا میشود اشیا و حافظههای موقتی هستند که به صورت خودکار ساخته میشوند. یکی از بد نام ترین این حافظهها حافظهی مربوط به String در زبان #C است. رشتهها در #C اصطلاحا immutable هستند و با هر بار اتصال دو رشته (concat) یک رشته دیگر ساخته میشود، متاسفانه اکثر توابع کار با رشته همین مشکل را دارند.
یکی از راههای جلوگیری از تخصیص پویای حافظه استفاده از انواع Pool مثل Object Pool است، بدین صورت که ابتدا میزان مشخصی از شئ ای که قرار است استفاده شود میسازیم و درون یک ظرف(container) نگهداری میکنیم، به جای اختصاص حافظهی پویا، شئ را از Pool درخواست میکنیم و بعد از استفاده دوباره آن را به Pool باز میگردانیم.
با توجه به نحوهی کار کردن حافظهی مدیریت شده در یونیتی باید به نکات زیر توجه داشت:
public class MyEnumComparer : IEqualityComparer<MyEnum> { public bool Equals(MyEnum x, MyEnum y) { return x == y; } public int GetHashCode(MyEnum x) { return (int)x; } }
public class MyClass { public static void UseParams(params int[] list) { for (int i = 0; i < list.Length; i++) { Console.Write(list[i] + " "); } Console.WriteLine(); } static void Main() { // You can send a comma-separated list of arguments of the // specified type. UseParams(1, 2, 3, 4); UseParams(1, 'a', "test"); } }
ParticleSystem.SetParticles public void SetParticles(out Particle[] particles, int size); public void SetParticles(out Particle[] particles, int size, int offset);
for (int i = 0; i < Input.touches.Length; i++ ) { Touch touch = Input.touches[i]; // … }
منابع:
https://blogs.unity3d.com/2016/12/05/unity-webgl-memory-the-unity-heap/
https://docs.unity3d.com/Manual/UnderstandingAutomaticMemoryManagement.html
https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity2.html
https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity4-1.html