Optimizing garbage collection in Unity

13
Trần Van Hải viết hơn 5 năm trước

Quản lý bộ nhớ trong unity

Để hiểu về garbage collection hoặt động và xảy ra thế nào , trước hết chúng ta cần tìm hiểu về quản lý bộ nhớ trong unity

Ở cấp độ cơ bản nhất, quản lý bộ nhớ tự động trong Unity hoạt động như sau: Unity có quyền truy cập vào hai nhóm bộ nhớ: stack và heap (Stack được sử dụng để lưu trữ ngắn hạn các mẩu dữ liệu nhỏ và heap được sử dụng để lưu trữ dài hạn và các phần dữ liệu lớn hơn.)

Khi một biến được tạo, Unity yêu cầu một khối bộ nhớ từ ngăn xếp hoặc heap, miễn là biến nằm trong phạm vi (vẫn có thể truy cập trong code), bộ nhớ được gán cho nó vẫn được sử dụng. có nghĩa bộ nhớ này đã được phân bổ. Các vùng bộ nhớ này có thể hiểu như các object trên stack và heap.

Khi biến đi ra khỏi phạm vi, bộ nhớ không còn cần thiết và có thể được trả về nhóm mà nó xuất phát. Khi bộ nhớ được trả về nhóm của nó, chúng ta nói rằng bộ nhớ đã bị giải phóng. Bộ nhớ từ ngăn xếp được giải phóng ngay khi biến đang được truy cập đi ra khỏi phạm vi. Tuy nhiên, bộ nhớ từ heap không được trả về vào thời điểm này và vẫn ở trạng thái được phân bổ mặc dù biến mà nó đề cập đến nằm ngoài phạm vi.

Trình thu gom rác xác định và giải phóng bộ nhớ heap không sử dụng. Trình thu gom rác được chạy định kỳ để dọn sạch heap.

Điều gì xảy ra trong quá trình cấp phát và thu hồi bộ nhớ trên Stack (Ngăn xếp)

Cấp phát và thu hồi bộ nhớ trong ngăn xếp nhanh chóng và đơn giản. Điều này là do ngăn xếp chỉ được sử dụng để lưu trữ dữ liệu nhỏ trong một khoảng thời gian ngắn. Phân bổ và thu hồi luôn xảy ra theo thứ tự dự đoán và có kích thước có thể dự đoán được.
Stack hoạt động giống như một kiểu dữ liệu ngăn xếp: nó là một tập hợp các phần tử đơn giản, trong trường hợp này là các khối bộ nhớ, trong đó các phần tử chỉ có thể được thêm và xóa theo một thứ tự nghiêm ngặt. Sự đơn giản và nghiêm ngặt này là điều khiến nó trở nên nhanh chóng: khi một biến được lưu trữ trên ngăn xếp, bộ nhớ cho nó được phân bổ đơn giản từ "Tail (Phần cuối)" của ngăn xếp. Khi một biến ngăn xếp vượt quá phạm vi, bộ nhớ được sử dụng để lưu trữ biến đó ngay lập tức được đưa trở lại ngăn xếp để sử dụng lại.

Điều gì xảy ra khi cấp phát và thu hồi bộ nhớ trên Heap

Phân bổ trong heap phức tạp hơn nhiều so với phân bổ ngăn xếp. Điều này là do heap có thể được sử dụng để lưu trữ cả dữ liệu dài hạn và ngắn hạn và dữ liệu thuộc nhiều loại và kích cỡ khác nhau. Phân bổ và thu hồi luôn xảy ra theo thứ tự có thể dự đoán được và có thể yêu cầu các khối bộ nhớ có kích thước rất khác nhau.
Khi một biến heap được tạo, các bước sau sẽ diễn ra:

  • Đầu tiên, Unity phải kiểm tra xem có đủ bộ nhớ trống trong heap không. Nếu có đủ bộ nhớ trống trong heap, bộ nhớ cho biến được phân bổ.
  • Nếu không có đủ bộ nhớ trống trong heap, Unity kích hoạt trình thu gom rác để giải phóng bộ nhớ heap không sử dụng. Điều này có thể làm chậm một khoảng thời gian. Sau đó nếu có đủ bộ nhớ trống trong heap, bộ nhớ cho biến được phân bổ.
  • Nếu có đủ bộ nhớ trống trong heap sau khi thu gom rác, Unity sẽ tăng lượng bộ nhớ trong heap. Điều này cũng có thể làm chậm. Bộ nhớ cho biến sau đó được phân bổ.
  • Phân bổ heap có thể chậm, đặc biệt nếu bộ thu gom rác phải chạy và heap cần phải mở rộng.

Điều gì xảy ra khi thu gom rác (Garbage Collection)

Khi một biến trong heap vượt quá phạm vi, bộ nhớ được sử dụng để lưu trữ nó không bị giải phóng ngay lập tức. Bộ nhớ heap không được sử dụng chỉ bị giải phóng khi bộ thu gom rác chạy.
Mỗi khi trình thu gom rác chạy, các bước sau sẽ xảy ra:

  • Trình thu gom rác kiểm tra mọi đối tượng trên heap.
  • Trình thu gom rác tìm kiếm tất cả các tham chiếu đối tượng hiện tại để xác định xem các đối tượng trên heap có còn trong phạm vi không.
  • Bất kỳ đối tượng không còn trong phạm vi được gắn cờ để xóa.
  • Các đối tượng được gắn cờ sẽ bị xóa và bộ nhớ được phân bổ cho chúng được trả về heap. Thu gom rác là một hoạt động phức tạp. Càng nhiều đối tượng trên heap, nó càng phải làm nhiều việc và càng có nhiều tham chiếu đối tượng trong code thì nó càng phải làm nhiều việc hơn.

Khi nào việc thu gom rác xảy ra?

Ba điều có thể khiến trình thu gom rác chạy:

  • Trình thu gom rác chạy bất cứ khi nào yêu cầu phân bổ heap không thể được thực hiện bằng bộ nhớ trống từ heap.
  • Trình thu gom rác tự động chạy theo thời gian (mặc dù tần suất thay đổi theo nền tảng).
  • Trình thu gom rác có thể bị buộc phải chạy thủ công. Thu gom rác có thể là một hoạt động thường xuyên. Trình thu gom rác được kích hoạt bất cứ khi nào phân bổ vùng heap không thể được thực hiện từ bộ nhớ heap có sẵn, điều đó có nghĩa là việc phân bổ và xử lý heap thường xuyên có thể dẫn đến việc thu gom rác thường xuyên.

Các vấn đề với bộ thu gom rác

Vấn đề rõ ràng nhất là bộ thu gom rác có thể mất một lượng thời gian đáng kể để chạy. Nếu trình thu gom rác có rất nhiều đối tượng trên heap hoặc rất nhiều tham chiếu đối tượng để kiểm tra, quá trình kiểm tra tất cả các đối tượng này có thể bị chậm. Điều này có thể khiến sản phẩm chậm, giật, lag.

Một vấn đề khác là bộ thu gom rác có thể chạy vào những thời điểm bất tiện. Nếu CPU đã làm việc chăm chỉ trong phần quan trọng về hiệu năng trong trò chơi của chúng tôi, thì ngay cả một lượng nhỏ thời gian bổ sung từ bộ thu gom rác cũng có thể gây ra FPS thấp và performance thay đổi rõ rệt.

Một vấn đề khác ít rõ ràng hơn là phân mảnh heap. Khi bộ nhớ được phân bổ từ heap, nó được lấy từ không gian trống trong các khối có kích thước khác nhau tùy thuộc vào kích thước của dữ liệu phải được lưu trữ. Khi các khối bộ nhớ này được trả về heap, heap có thể được chia thành nhiều khối nhỏ miễn phí được phân tách bằng các khối được phân bổ. Điều này có nghĩa là mặc dù tổng dung lượng bộ nhớ trống có thể cao nhưng không thể phân bổ các khối bộ nhớ lớn mà không chạy bộ thu gom rác hoặc mở rộng vùng heap vì không có khối nào đủ lớn.

Có hai vấn đề khi một heap phân mảnh. Đầu tiên là việc sử dụng bộ nhớ trong trò chơi sẽ cao hơn mức cần thiết và thứ hai là trình thu gom rác sẽ chạy thường xuyên hơn.

Phân bổ Heap

Nếu chúng ta biết rằng bộ thu gom rác đang gây ra sự cố trong game, chúng ta cần biết phần nào trong code của chúng ta đang tạo ra rác. Rác được tạo ra khi các biến trên heap đi ra khỏi phạm vi, vì vậy trước tiên chúng ta cần biết nguyên nhân gây ra một biến được phân bổ trên heap.

Cái gì được phân bổ trên Stack và Heap?

Trong Unity, các biến cục bộ có giá trị được phân bổ trên ngăn xếp và mọi thứ khác được phân bổ trên heap.

Đoạn code sau đây là một ví dụ về phân bổ ngăn xếp, vì biến localInt là biến cục bộ và chứa giá trị. Bộ nhớ được phân bổ cho biến này sẽ được giải phóng khỏi ngăn xếp ngay sau khi chức năng này chạy xong.

void ExampleFunction()
{
    int localInt = 5;
}

Đoạn code sau đây là một ví dụ về phân bổ heap, vì biến localList là cục bộ nhưng có giá trị tham chiếu. Bộ nhớ được phân bổ cho biến này sẽ bị giải phóng khi bộ thu gom rác chạy.

void ExampleFunction()
{
    List localList = new List();
}

Sử dụng Unity Profiler Window để tìm ra vị trí phân bổ trên heap

alt text

Chúng ta mở Profiler lên và quan sát được hàm nào gây ra phân bổ heap và nó tốn bao nhiêu bộ nhớ

Giảm thiểu tác động của việc thu gom rác

Nói rộng hơn, chúng ta có thể giảm tác động của việc thu gom rác lên trò chơi của mình theo ba cách:

  • Giảm thời gian mà trình thu gom rác mất để chạy.
  • Giảm tần suất mà trình thu gom rác chạy.
  • Chúng ta có thể cố tình kích hoạt trình thu gom rác để nó chạy vào những thời điểm không quan trọng về hiệu năng, ví dụ như trong màn hình Loading hay khi pause. Với ý nghĩ đó, có ba hướng để xử lý :
  • Chúng ta có thể tổ chức trò chơi của mình để chúng tôi có ít phân bổ heap hơn và ít tham chiếu đối tượng hơn. Ít đối tượng trên heap và ít tham chiếu hơn để kiểm tra có nghĩa là khi bộ sưu tập rác được kích hoạt, sẽ mất ít thời gian hơn để chạy.
  • Chúng ta có thể giảm tần suất phân bổ và thu hồi bộ nhớ, đặc biệt là vào thời điểm quan trọng về hiệu suất. Việc phân bổ và giải quyết ít hơn có nghĩa là ít lần kích hoạt thu gom rác hơn. Điều này cũng làm giảm nguy cơ phân mảnh heap.
  • Chúng ta có thể cố gắng thu thập time garbage và mở rộng heap để chúng xảy ra vào thời điểm thuận tiện và có thể dự đoán được. Đây là một cách tiếp cận khó khăn và kém tin cậy hơn, nhưng khi được sử dụng như một phần của chiến lược quản lý bộ nhớ tổng thể nó có thể làm giảm tác động của việc thu gom rác.

Giảm lượng rác tạo ra

Có thể giảm lượng rác tạo ra bằng cách sử dụng một vài kỹ thuật trong code:

Caching (Bộ nhớ đệm)

Nếu code của chúng ta liên tục gọi các hàm dẫn đến phân bổ heap và sau đó loại bỏ kết quả nó sẽ tạo ra rác không cần thiết. Thay vào đó, chúng ta nên lưu trữ các tham chiếu đến các đối tượng này và tái sử dụng chúng. Kỹ thuật này được gọi là bộ nhớ đệm.

Trong ví dụ sau, mã gây ra phân bổ heap mỗi lần nó được gọi. Điều này xảy ra là do một mảng mới được tạo ra.

void OnTriggerEnter(Collider other)
{
    Renderer[] allRenderers = FindObjectsOfType<Renderer>();
    ExampleFunction(allRenderers);
}

Đoạn code sau chỉ gây ra một phân bổ heap, vì mảng được tạo và được ghi một lần rồi lưu vào bộ đệm. Mảng lưu trữ có thể được sử dụng lại nhiều lần mà không tạo thêm rác.

private Renderer[] allRenderers;

void Start()
{
    allRenderers = FindObjectsOfType<Renderer>();
}


void OnTriggerEnter(Collider other)
{
    ExampleFunction(allRenderers);
}

Không phân bổ các functions được gọi thường xuyên

Nếu chúng ta phải phân bổ bộ nhớ heap trong MonoBehaviour, thì nơi tồi tệ nhất chúng ta có thể làm là trong các chức năng chạy thường xuyên. Ví dụ, Update() và lateUpdate (), được gọi một lần trên mỗi khung, vì vậy nếu code mà tạo rác ở đây, nó sẽ nhanh chóng tăng lên. Chúng ta nên xem xét các tham chiếu bộ đệm tới các đối tượng trong Start () hoặc Awake () nếu có thể hoặc đảm bảo rằng mã gây ra phân bổ chỉ chạy khi cần.
Một ví dụ rất đơn giản về việc di chuyển mã để nó chỉ chạy khi mọi thứ thay đổi. Khi một hàm gây ra sự phân bổ được gọi mỗi khi Update () được gọi, nó sẽ tạo rác thường xuyên:

void Update()
{
    ExampleGarbageGeneratingFunction(transform.position.x);
}

Với sự thay đổi nhỏ chúng ta có thể chắc rằng hàm được phân bổ chỉ được gọi khi giá trị transform.position.x thay đổi, Chúng sẽ tạo ra heap allocations khi cần thiết thay vì được gọi liên tục ở mỗi frame

private float previousTransformPositionX;

void Update()
{
    float transformPositionX = transform.position.x;
    if (transformPositionX != previousTransformPositionX)
    {
        ExampleGarbageGeneratingFunction(transformPositionX);
        previousTransformPositionX = transformPositionX;
    }
}

Một kỹ thuật khác để giảm rác được tạo trong Update () là sử dụng timer. Điều này phù hợp khi chúng ta có mã tạo rác phải chạy thường xuyên, nhưng không nhất thiết là mọi khung.

Trong mã ví dụ sau, hàm tạo rác chạy một lần trên mỗi khung:

void Update()
{
    ExampleGarbageGeneratingFunction();
}

Trong trường hợp này ta sử dụng 1 timer để cahwcs chắn rằng nó sẽ chỉ tạo ra rác khi chạy ở mỗi giây

private float timeSinceLastCalled;

private float delay = 1f;

void Update()
{
    timeSinceLastCalled += Time.deltaTime;
    if (timeSinceLastCalled > delay)
    {
        ExampleGarbageGeneratingFunction();
        timeSinceLastCalled = 0f;
    }
}

Những thay đổi nhỏ như thế này, khi được thực hiện để mã chạy thường xuyên, có thể giảm đáng kể lượng rác được tạo ra.

Clear Collections

Tạo các bộ sưu tập mới gây ra sự phân bổ trên heap. Khi tạo ra các bộ sưu tập mới nhiều lần trong mã của mình, chúng tôi nên lưu trữ tham chiếu đến bộ sưu tập và sử dụng Clear () để làm trống nội dung của nó thay vì gọi mới liên tục.
Trong ví dụ sau, phân bổ heap mới xảy ra mỗi khi * new * được sử dụng.

void Update()
{
    List myList = new List();
    PopulateList(myList);
}

Trong ví dụ sau, phân bổ chỉ xảy ra khi bộ sưu tập được tạo hoặc khi bộ sưu tập phải được thay đổi kích thước khi chạy ngầm. Điều này làm giảm đáng kể lượng rác tạo ra.

private List myList = new List();
void Update()
{
    myList.Clear();
    PopulateList(myList);
}

Object pooling

Ngay cả khi chúng ta giảm phân bổ trong các tập lệnh của mình, chúng ta vẫn có thể gặp sự cố thu gom rác nếu như tạo và phá hủy nhiều đối tượng khi chạy. Object pooling là một kỹ thuật có thể giảm phân bổ và giải quyết bằng cách sử dụng lại các đối tượng thay vì liên tục tạo và phá hủy chúng. Object pooling được sử dụng rộng rãi trong các trò chơi và phù hợp nhất cho các tình huống chúng ta thường xuyên sinh ra và phá hủy các vật thể tương tự; ví dụ như khi bắn đạn từ súng.

Nguyên nhân phổ biến của phân bổ đống không cần thiết

Chúng tôi hiểu rằng các biến cục bộ, giá trị được phân bổ trên ngăn xếp và mọi thứ khác được phân bổ trên heap. Tuy nhiên, có rất nhiều tình huống mà việc phân bổ heap có thể khiến chúng ta bất ngờ. Hãy cùng xem một vài nguyên nhân phổ biến của việc phân bổ đống không cần thiết và xem xét cách tốt nhất để giảm những thứ này.

String

Trong C #, chuỗi là loại tham chiếu không phải là loại tham trị, mặc dù chúng dường như giữ "giá trị" của chuỗi. Điều này có nghĩa là việc tạo và loại bỏ các chuỗi tạo ra rác. Vì các chuỗi thường được sử dụng trong rất nhiều code, rác này thực sự có thể tăng lên.

Các chuỗi trong C # cũng không thay đổi, điều đó có nghĩa là giá trị của chúng có thể được thay đổi sau khi chúng được tạo lần đầu tiên. Mỗi lần chúng ta thao tác một chuỗi (ví dụ: bằng cách sử dụng toán tử + để nối hai chuỗi), Unity tạo một chuỗi mới với giá trị được cập nhật và loại bỏ chuỗi cũ. Điều này tạo ra rác.

Chúng ta có thể làm theo một vài quy tắc đơn giản để giữ rác từ các chuỗi ở mức tối thiểu. Hãy xem xét các quy tắc này, sau đó xem xét một ví dụ về cách áp dụng chúng.

  • Chúng ta nên cắt giảm việc tạo chuỗi không cần thiết. Nếu chúng ta đang sử dụng cùng một giá trị chuỗi nhiều lần, chúng ta nên tạo chuỗi một lần và lưu trữ giá trị.

  • Chúng ta nên cắt giảm các thao tác chuỗi không cần thiết. Ví dụ: nếu chúng ta có một thành phần Văn bản được cập nhật thường xuyên và chứa một chuỗi nối, chúng ta có thể xem xét tách nó thành hai thành phần.

  • Nếu chúng ta phải xây dựng các chuỗi khi chạy, chúng ta nên sử dụng lớp StringBuilder. Lớp StringBuilder được thiết kế để xây dựng các chuỗi không phân bổ và sẽ tiết kiệm lượng rác chúng ta tạo ra khi nối các chuỗi phức tạp.

  • Chúng ta nên xóa các cuộc gọi đến Debug.Log () ngay khi chúng không còn cần thiết cho mục đích gỡ lỗi. Các lệnh gọi tới Debug.Log () vẫn thực thi trong tất cả các bản dựng của trò chơi của chúng tôi, ngay cả khi chúng không xuất ra bất cứ thứ gì. Một cuộc gọi đến Debug.Log () tạo và loại bỏ ít nhất một chuỗi, vì vậy nếu trò chơi của chúng tôi chứa nhiều cuộc gọi này, rác có thể tăng lên.

Hãy cùng kiểm tra một ví dụ về mã tạo ra rác không cần thiết thông qua việc sử dụng chuỗi không hiệu quả. Trong đoạn code sau, chúng ta tạo một chuỗi để hiển thị điểm số trong Update () bằng cách kết hợp chuỗi "TIME:" với giá trị của bộ đếm thời gian float. Điều này tạo ra rác không cần thiết.

public Text timerText;
private float timer;

void Update()
{
    timer += Time.deltaTime;
    timerText.text = "TIME:" + timer.ToString();
}

Trong ví dụ sau, chúng ta đã cải thiện mọi thứ đáng kể. Chúng tôi đặt từ "TIME:" trong một thành phần riêng và đặt giá trị của nó trong Start (). Điều này có nghĩa là trong Update (), chúng ta không còn phải kết hợp các chuỗi. Điều này làm giảm lượng rác được tạo ra đáng kể.

public Text timerHeaderText;
public Text timerValueText;
private float timer;

void Start()
{
    timerHeaderText.text = "TIME:";
}

void Update()
{
    timerValueText.text = timer.toString();
}

Unity function calls

Điều quan trọng cần lưu ý là bất cứ khi nào chúng tôi gọi code mà chúng tôi đã tự viết, cho dù đó là bản thân trong Unity hay trong một plugin, nó có thể tạo ra rác. Một số lệnh gọi hàm Unity tạo phân bổ heap và do đó nên được sử dụng cẩn thận để tránh tạo rác không cần thiết.

Không có danh sách các chức năng mà chúng ta nên tránh. Mọi chức năng có thể hữu ích trong một số tình huống và ít hữu ích hơn trong các tình huống khác. Cách tốt nhất là xem xét chúng một cách cẩn thận, xác định nơi rác được tạo ra và suy nghĩ cẩn thận về cách xử lý nó. Trong một số trường hợp, có thể lưu trữ kết quả của chức năng; trong các trường hợp khác, có thể là tốt hơn khi gọi hàm ít thường xuyên hơn; trong các trường hợp khác, tốt nhất là nên cấu trúc lại code để sử dụng một hàm khác.
Mỗi khi chúng ta truy cập vào một hàm Unity trả về một mảng, một mảng mới sẽ được tạo và chuyển cho chúng ta làm giá trị trả về. Hành vi này không phải là luôn luôn rõ ràng hoặc được mong đợi, đặc biệt là khi chức năng là một trình truy cập (ví dụ: Mesh.normals).

Trong đoạn mã sau, một mảng mới được tạo cho mỗi lần lặp của vòng lặp.

void ExampleFunction()
{
    for (int i = 0; i < myMesh.normals.Length; i++)
    {
        Vector3 normal = myMesh.normals[i];
    }
}

Nó dễ dàng giảm phân bổ trong các trường hợp như thế này: chúng ta chỉ cần lưu trữ một tham chiếu đến mảng. Khi chúng ta làm điều này, chỉ có một mảng được tạo và lượng rác được tạo ra sẽ giảm đi tương ứng.

Đoạn code sau đây chứng minh điều này. Trong trường hợp này, chúng ta đã gọi Mesh.normals trước khi vòng lặp chạy và lưu trữ tham chiếu để chỉ một mảng được tạo.

void ExampleFunction()
{
    Vector3[] meshNormals = myMesh.normals;
    for (int i = 0; i < meshNormals.Length; i++)
    {
        Vector3 normal = meshNormals[i];
    }
}

Một nguyên nhân bất ngờ khác của phân bổ heap có thể được tìm thấy trong các hàm GameObject.name hoặc GameObject.tag. Cả hai đều là các bộ truy cập trả về các chuỗi mới, có nghĩa là việc gọi các hàm này sẽ tạo ra rác. Bộ nhớ đệm giá trị có thể hữu ích, nhưng trong trường hợp này có chức năng Unity liên quan mà chúng ta có thể sử dụng thay thế. Để kiểm tra thẻ GameObject trong một giá trị mà không tạo ra rác, chúng ta có thể sử dụng GameObject.CompareTag ().

Trong code ví dụ sau, rác được tạo bởi lệnh gọi đến GameObject.tag:

private string playerTag = "Player";

void OnTriggerEnter(Collider other)
{
    bool isPlayer = other.gameObject.tag == playerTag;
}

Nếu chúng tôi sử dụng GameObject.CompareTag (), chức năng này không còn tạo ra bất kỳ rác nào:

private string playerTag = "Player";

void OnTriggerEnter(Collider other)
{
    bool isPlayer = other.gameObject.CompareTag(playerTag);
}

GameObject.CompareTag không chỉ duy nhất , nhiều lệnh gọi hàm Unity có các phiên bản thay thế không gây ra sự phân bổ heap. Ví dụ: chúng ta có thể sử dụng Input.GetTouch () và Input.touchCount thay cho Input.touches hoặc Physics.SphereCastNonAlloc () thay cho Physics.SphereCastAll ().

Boxing

Boxing là thuật ngữ cho những gì xảy ra khi một biến chứa giá trị được sử dụng thay cho biến được nhập tham chiếu. Boxing thường xảy ra khi chúng ta chuyển các biến được gõ giá trị, chẳng hạn như ints hoặc float, cho một hàm có tham số đối tượng như Object.Equals ().

Ví dụ, hàm String.Format () nhận một chuỗi và một tham số đối tượng. Khi chúng ta truyền cho nó một chuỗi và một int, int phải được đóng hộp. Do đó, đoạn mã sau có chứa một ví dụ về Boxing:

void ExampleFunction()
{
    int cost = 5;
    string displayString = String.Format("Price: {0} gold", cost);
}

Boxing tạo ra rác vì những gì xảy ra ngầm phía sau. Khi một biến được gõ giá trị được đóng hộp, Unity tạo một System.Object tạm thời trên heap để bọc biến giá trị được gõ. System.Object là một biến được tham chiếu, vì vậy khi đối tượng tạm thời này được xử lý, nó sẽ tạo ra rác.

Boxing là một nguyên nhân cực kỳ phổ biến của việc phân bổ đống không cần thiết. Ngay cả khi chúng tôi tặng các biến số hộp trực tiếp trong code chúng ta có thể đang sử dụng các plugin gây ra Boxing hoặc nó có thể chạy ngầm dưới các chức năng khác.

Coroutines

Gọi StartCoroutine () tạo ra một lượng rác nhỏ, vì các lớp mà Unity phải tạo các Instance để quản lý coroutine. Với ý nghĩ đó, các cuộc gọi đến StartCoroutine () nên bị hạn chế trong khi trò chơi tương tác và hiệu suất là một mối quan tâm. Để giảm rác được tạo theo cách này, bất kỳ coroutine nào phải chạy vào thời điểm quan trọng về hiệu năng nên được bắt đầu trước và chúng ta đặc biệt cẩn thận khi sử dụng các coroutine lồng nhau có thể chứa các cuộc gọi bị trì hoãn đến StartCoroutine ().yield, coroutines không tạo ra phân bổ heap theo cách riêng của nó; tuy nhiên, các giá trị chúng ta khi qua " yield" có thể tạo ra các phân bổ heap không cần thiết. Ví dụ: đoạn mã sau tạo rác:

yield return 0;

Mã này tạo rác vì int có giá trị 0 được retuned. Trong trường hợp này, nếu chúng ta chỉ muốn đợi một khung mà không gây ra bất kỳ phân bổ heap nào, cách tốt nhất để làm điều đó là :

yield return null;

Một lỗi phổ biến khác với coroutines là sử dụng mới khi mang lại cùng một giá trị hơn một lần. Ví dụ: đoạn code sau sẽ tạo và sau đó loại bỏ một đối tượng WaitForSeconds mỗi khi vòng lặp lặp:

while (!isComplete)
{
    yield return new WaitForSeconds(1f);
}

Nếu chúng ta lưu trữ và sử dụng lại đối tượng WaitForSeconds, rác sẽ được tạo ra ít hơn nhiều. Đoạn code sau đây cho thấy đây là một ví dụ:

WaitForSeconds delay = new WaitForSeconds(1f);

while (!isComplete)
{
    yield return delay;
}

foreach loops

Phiên bản Unity 5.5 đã loại bỏ hầy hết các tác động của foreach loops đến perfomance

Cấu trúc code để giảm tác động của garbage collection

Cách mà code được cấu trúc có thể tác động đến việc thu gom rác. Ngay cả khi mã của chúng tôi không tạo phân bổ heap, nó có thể thêm vào khối lượng công việc của bộ thu gom rác.

Code có thể thêm một số lượng hành động không cần thiết vào khối lượng công việc của bộ thu gom rác là bằng cách yêu cầu nó kiểm tra những thứ mà nó không cần phải kiểm tra. Structs là các biến được gõ giá trị, nhưng nếu chúng ta có một cấu trúc có chứa biến được nhập tham chiếu thì trình thu gom rác phải kiểm tra toàn bộ cấu trúc. Nếu chúng ta có một mảng lớn các cấu trúc này, thì điều này có thể tạo ra rất nhiều công việc bổ sung cho trình thu gom rác.

Trong ví dụ này, struct chứa một chuỗi, được gõ tham chiếu. Toàn bộ các cấu trúc bây giờ phải được kiểm tra bởi người thu gom rác khi nó chạy.

public struct ItemData
{
    public string name;
    public int cost;
    public Vector3 position;
}
private ItemData[] itemData;

Trong ví dụ này, struct chứa một chuỗi, được gõ tham chiếu. Toàn bộ các cấu trúc bây giờ phải được kiểm tra bởi người thu gom rác khi nó chạy.

private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;

Một cách khác mà mã của chúng tôi có thể thêm một cách không cần thiết vào khối lượng công việc của bộ thu gom rác là bằng cách có các tham chiếu đối tượng không cần thiết. Khi trình thu gom rác tìm kiếm các tham chiếu đến các đối tượng trên heap, nó phải kiểm tra mọi tham chiếu đối tượng hiện tại trong mã của chúng ta. Có ít tham chiếu đối tượng trong mã của chúng tôi có nghĩa là nó có ít việc phải làm hơn, ngay cả khi chúng tôi không làm giảm tổng số đối tượng trong heap.

Trong ví dụ này, chúng ta có một lớp điền vào hộp thoại. Khi người dùng đã xem hộp thoại, một hộp thoại khác sẽ được hiển thị. Mã của chúng tôi chứa tham chiếu đến phiên bản tiếp theo của DialogData sẽ được hiển thị, có nghĩa là trình thu gom rác phải kiểm tra tham chiếu này như một phần của hoạt động của nó:

public class DialogData
{
    private DialogData nextDialog;

    public DialogData GetNextDialog()
    {
        return nextDialog;
    }
}

Ở đây, chúng tôi đã cấu trúc lại mã để nó trả về một mã định danh được sử dụng để tra cứu phiên bản tiếp theo của DialogData, thay vì chính cá thể đó. Đây không phải là một tham chiếu đối tượng, vì vậy nó không thêm vào thời gian của trình thu gom rác.

public class DialogData
{
    private int nextDialogID;

    public int GetNextDialogID()
    {
        return nextDialogID;
    }
}

Ví dụ này khá tầm thường. Tuy nhiên, nếu trò chơi của chúng tôi chứa rất nhiều đối tượng chứa các tham chiếu đến các đối tượng khác, chúng tôi có thể giảm đáng kể độ phức tạp của đống bằng cách cấu trúc lại mã của chúng tôi theo cách này.

Thời gian thu gom rác

Buộc thu gom rác thủ công

Cuối cùng, chúng tôi có thể muốn kích hoạt bộ sưu tập rác. Nếu chúng tôi biết rằng bộ nhớ heap đã được phân bổ nhưng không còn được sử dụng (ví dụ: nếu mã của chúng tôi đã tạo rác khi tải tài sản) và chúng tôi biết rằng việc thu gom rác đóng băng sẽ không ảnh hưởng đến người chơi (ví dụ: trong khi màn hình tải vẫn đang hiển thị), chúng tôi có thể yêu cầu thu gom rác bằng mã sau:

System.GC.Collect();

Điều này sẽ buộc người thu gom rác chạy, giải phóng bộ nhớ không sử dụng tại thời điểm thuận tiện cho chúng ta.

Bình luận


White
{{ comment.user.name }}
Hay Bỏ hay
{{ comment.like_count}}
White

Trần Van Hải

44 bài viết.
0 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}

{{like_count}}

kipalog

{{ comment_count }}

Bình luận


White
{{userFollowed ? 'Following' : 'Follow'}}
44 bài viết.
0 người follow

 Đầu mục bài viết