0

Quản lý bộ nhớ trong Unity

1. Giới thiệu

Câu hỏi của chúng ta cần giải đáp trong bài viết này là làm thế nào để quản lý bộ nhớ trong project Unity .

Khi chúng ta tạo một mảng, chuỗi hoặc đối tượng thì sau đó bộ nhớ sẽ giao cho một vùng nhớ được gọi là HEAP. Khi những thứ không được sử dụng trong một thời gian dài thì bộ nhớ đó sẽ được sử dụng cho những thứ khác. Trước đây thì các lập trình viên phải allocate và release các khối bộ nhớ HEAP một cách rõ ràng với các hàm gọi chức năng liên quan . Bây giờ thì việc quản lý bộ nhớ được thực hiện tự động bởi hệ thống thời gian thực giống như là Unity’s Mon develop engine. Quản lý bộ nhớ tự động cho phép công sức viết code đơn giản rõ ràng hơn và giảm thiểu khả năng rò rỉ bộ nhớ .

2. Reference and Value Types

Khi bất kỳ hàm nào được gọi, sau đó các giá trị của các đối số của nó sẽ được sao chép vào một vùng của bộ nhớ dành riêng cho cuộc gọi riêng biệt. Các kiểu dữ liệu, cái mà cấp phát chỉ một vài byte, có thể được sao chép rất nhanh chóng và dễ dàng. Tuy nhiên, nó được phổ biến cho các mảng, các đối tượng và các chuỗi và nó sẽ rất là không hiệu quả nếu những loại dữ liệu đó được sao chép một cách thường xuyên. May mắn thay, điều này là không bắt buộc; bộ nhớ lưu trữ thực tế cho một mục lớn được cấp phát từ một 'con trỏ' chỉ chiếm một vùng nhớ rất nhỏ và giá trị heap được sử dụng để ghi nhớ "địa chỉ" bộ nhớ của nó. Sau đó chỉ có con trỏ cần phải được sao chép thông qua đối số. Các danh mục, được biết đến bởi con trỏ, có thể được định vị bằng hệ thống thời gian thực, một bản duy nhất của một mục dữ liệu có thể sử dụng theo yêu cầu. Kiểu mà có thể được sao chép và lưu trữ trực tiếp trong quá trình chạy được gọi là các kiểu giá trị. Chúng bao gồm char, float, integer, Boolean và các kiểu struct trong Unity của (ví dụ, color và Vector3). Các kiểu được lưu trữ trong vùng heap và sau đó truy cập thông qua con trỏ được gọi là kiểu tham chiếu, vì giá trị được lưu trữ trong biến chỉ đề cập đến các dữ liệu thực tế. Strings, object và mảng là những ví dụ của các kiểu dữ liệu này .

3. Memory Allocation and Garbage Collection

Vùng nhớ chưa sử dụng của HEAP được liên tục theo dõi bởi trình quản lý bộ nhớ. Trình quản lý bộ nhớ sẽ phân bổ không gian không sử dụng khi một khối mới của bộ nhớ được yêu cầu. Thủ tục này sẽ xảy ra cho đến khi các yêu cầu tiếp theo đã được xử lý. Nó không chắc rằng mỗi bộ nhớ được phân bổ từ HEAP vẫn được sử dụng. Một mục tài liệu tham khảo trên HEAP có thể được truy cập miễn là nó vẫn còn đang tham chiếu tới các biến mà có thể xác định vị trí nó. Nếu tất cả các tài liệu tham khảo của một khối bộ nhớ được cấp phát và sau đó bộ nhớ đó chiếm một cách an toàn thì nó có thể được cấp phát lại. Để xác định được khối đống không sử dụng, trình quản lý bộ nhớ tìm kiếm qua tất cả các biến tham chiếu đang hoạt động và đánh dấu chúng là 'live'. Vào cuối của quá trình tìm kiếm, quản lý bộ nhớ xem xét bất kỳ không gian giữa các khối bộ nhớ trực tiếp khi trống rỗng, và cũng có thể được sử dụng để cấp phát bộ nhớ tiếp theo. Quá trình định vị và giải phóng bộ nhớ không sử dụng được gọi là quá trình thu gom rác (GC).

4. Memory Optimization

GC không phải làm bằng tay và không thể được nhìn thấy bởi các lập trình viên nhưng quá trình thu thập thực sự đòi hỏi đơn vị xử lý trung tâm (CPU) thời gian đáng kể . Khi được sử dụng một cách chính xác, quản lý bộ nhớ tự động nói chung sẽ bằng hoặc vượt cấp phát bộ nhớ so với tổng sử dụng thực tế. Tuy nhiên, nó là cần thiết cho các nhà phát triển để tránh những sai lầm nó sẽ kích hoạt các collector thường xuyên hơn cần thiết và khuyến nghị tạm dừng trong quá trình chạy. Có một số thuật toán có thể là cơn ác mộng của Garbage Collection cho dù nó có vẻ vô tội ngay từ cái nhìn đầu tiên.

5. Example

functionStringConatinationExample (integerArray: int[])
{
varmyLine=integerArray[0].ToString();

for(var i=1; i<integerArray.Length;i++)
{
myLine +=,+integerArray[i].ToString();
}
return myLine;
}

Điều cần tìm hiểu ở đây là các phần mới của chuỗi không được thêm vào cùng một lúc mà là từng bước một . Điều thực sự xảy ra là mỗi lần lặp, những nội dung cuối cùng của biến myLine có thể chết - một chuỗi hoàn toàn mới được chỉ định để chứa các mảng ban đầu cộng với các mảng mới ở cuối. Từ đó chuỗi cứ lớn hơn với các giá trị của biến ngày càng tăng, giá trị của không gian HEAP được tiêu thụ cũng tăng lên và vì thế nó rất dễ bị sử dụng lên hàng ngàn byte của bộ nhớ mỗi lần hàm này được gọi.

Để nối chuỗi, sử dụng lớp System.Text.StringBuilder. Tuy nhiên, việc nối chuỗi lặp đi lặp lại sẽ không gây ra nhiều rắc rối, trừ khi nó được gọi lại thường xuyên, và trong Unity thì rất hay gọi hàm update với số lần gọi là 60 lần trên giây thì nó là vấn đề rất lớn .

varmyScoreBoard: GUIText;
varmyScore: int;
function Update()
{
var myScoreText: String=”My Score:+myScore.ToString();
myScoreBoard.text=myScoreTxt;
}

Đoạn code trên sẽ cấp phát một vùng nhớ cho một biến mới mỗi khi hàm Update được gọi và đồng thời bộ gom rác cũng chạy liên tục . Trong trường hợp này có thể lưu text bằng cách cập nhật text mỗi khi thay đổi giá trị của myScoreBoard .

5.1 Updated Example 1

var myScoreBoard: GUIText;
var myScoreText: String;
var myScore: int;
var myOldScore: int;
function Update()
{
if(myScore != myOldScore)
{
myScoreText = “My Score:+myScore.ToString();
myScoreBoard.text = myScoreText;
myOldScore = myScore;
}
}

Có một vấn đề xảy ra ở đây là một mảng được trả về từ một hàm .

functionRandomList(numberOfElements:int)
{
varmyResult=new float[numberOfElements];
for(var i=0; i<numberOfElements; i++)
{
myResult[i]=Random.value;
}
return myResult;
}

Hàm kiểu này là rất đẹp và tiện lợi khi các mảng mới được lấp đầy bởi các giá trị. Tuy nhiên, nếu nó được gọi liên tục sau đó bộ nhớ sẽ được cấp phát mỗi lần. Kể từ khi các mảng có thể rất lớn, không gian HEAP free sẽ được sử dụng nhiều lên nhanh chóng, dẫn đến bộ thu gom rác lặp đi lặp lại. Để tránh vấn đề này, hãy sử dụng mảng là một kiểu tham chiếu. Một mảng thông qua vào một hàm như một tham số có thể thay đổi và kết quả sẽ vẫn còn sau khi return.

5.2 Updated Example 2

functionRandomList(myArray: int)
{
for(var i=0;i<myArray.Length;i++)
{
myArray[i]=Random.value;
}
}

Điều này chỉ đơn giản là thay thế các giá trị hiện có của mảng với các giá trị mới nhất. Mặc dù điều này đòi hỏi việc cấp phát ban đầu của mảng được hoàn thành trước. Bộ thu gom rác Như đã đề cập, nó là rất tốt để tránh cấp phát lãng phí. Tuy nhiên, nó không thể loại bỏ hoàn toàn, có hai chiến lược chính, bạn có thể sử dụng để làm giảm instruction trong gameplay.

5.3 Quick and frequent garbage collection (with Small Heap)

Chiến lược này là tốt nhất cho những trò chơi có gameplay lớn, nơi mà yêu cầu khung hình trơn tru là điều chủ yếu. Một trò chơi thường cấp phát khối kích thước nhỏ thường xuyên, nhưng những khối có thể sử dụng chỉ trong một thời gian ngắn. Kích thước của HEAP khi sử dụng chiến lược này trên iOS là khoảng 200KB và bộ thu gom rác sẽ mất khoảng 5ms trên iPhone 3G. Nếu sự gia tăng kích thước HEAP lên 1MB, bộ thu gom rác sẽ mất khoảng 7ms. Nó sẽ có lợi cho yêu cầu thu gom rác tại một khoảng thời gian khung hình thường xuyên. Điều này thường sẽ làm cho thu gom rác xảy ra thường xuyên hơn thực sự cần thiết nhưng họ sẽ được xử lý một cách nhanh chóng và có hiệu lực tối thiểu trên gameplay

if(Time.frameCount % 30 ==0)
{
System.GC.Collect();
}

Tuy nhiên, bạn nên sử dụng kỹ thuật này và kiểm tra số liệu thống kê để chắc chắn rằng nó thực sự là giảm thời gian thu gom rác. Chậm nhưng không thường xuyên thu gom rác (với Heap lớn) Chiến lược này là tốt nhất cho những game mà cấp phát tương đối thường xuyên và có thể được xử lý trong khi tạm dừng gameplay. Nó là hữu ích cho các HEAP để được lớn nhất có thể mà không bị quá lớn tới mức trò chơi của bạn bị kill bởi các hệ điều hành do bộ nhớ hệ thống thấp. Tuy nhiên, thời gian chạy Mono tránh mở rộng các HEAP tự động nếu có thể. Bạn có thể mở rộng các HEAP bằng tay bằng cách preallocating một số không gian để giữ chỗ trong khởi động (ví dụ, bạn tạo một đối tượng "useless") được cấp phát hoàn toàn là do hệ thống quản lý bộ nhớ .

function Start()
{
var tmp =new System.Object[1024];
//make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
for(var i : int=0; i<1024; i++)
tmp[i]=new byte[1024]
}
//release reference
tmp=null;
}

Một heap đủ lớn nên không được hoàn toàn lấp đầy giữa những lúc tạm dừng trong gameplay mà sẽ chứa một bộ sưu tập. Khi tạm dừng như vậy xảy ra, bạn có thể yêu cầu một bộ sưu tập một cách rõ ràng

System.GC.Collect();

Một lần nữa, bạn nên cẩn thận khi sử dụng chiến lược này và chú ý đến số liệu thống kê chứ không phải chỉ là giả định nó là có hiệu quả mong muốn.

6 . Reuse of Object Pools

Có số lượng các trường hợp trong đó chúng ta có thể tránh tạo ra rác chỉ đơn giản bằng cách giảm số lượng các đối tượng được tạo ra và bị hủy. Có một số loại đối tượng trong các trò chơi, chẳng hạn như đạn, nó có thể tạo ra rất nhiều nhưng chỉ một số nhỏ trong chúng là được sử dụng. Trong những trường hợp như thế này, ta có thể sử dụng lại các đối tượng chứ không phải là phá hủy cái cũ và thay thế chúng bằng những cái mới.

Qua bài viết này tôi hi vọng các bạn sẽ có những cách giải quyết vấn đề bộ nhớ trong Unity một cách tốt nhất làm sao cho game của bạn có thể vận hành một cách trơn tru mà tốn ít bộ nhớ nhất có thể .


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.