Avoiding Garbage Collection Spikes by Reusing Lists in Unity

Introduction

In Unity, frequent memory allocations in performance-critical methods, such as Update, can lead to unnecessary garbage collection (GC) and frame drops. One common mistake is allocating new collections each frame, which can cause performance issues over time.

Problem: Allocating Lists Every Frame

Consider this example:

using System.Collections.Generic;
using UnityEngine;

public class ListTest : MonoBehaviour
{
    private List<Enemy> _enemies = new List<Enemy>();

    private void Start()
    {
        for (int i = 0; i < 1000; i++)
        {
            EnemyType enemyType = (EnemyType)Random.Range(0, 1);
            _enemies.Add(new Enemy(enemyType));
        }
    }

    private void Update()
    {
        FindEnemiesByType(EnemyType.Water);
    }

    private List<Enemy> FindEnemiesByType(EnemyType enemyType)
    {
        // New allocation every frame
        List<Enemy> matchingEnemies = new List<Enemy>(); 
        
        for (int i = 0; i < _enemies.Count; i++)
        {
            if (_enemies[i].EnemyType == enemyType)
                matchingEnemies.Add(_enemies[i]);
        }
        return matchingEnemies;
    }

    public class Enemy
    {
        private EnemyType _enemyType;
        public EnemyType EnemyType => _enemyType;

        public Enemy(EnemyType type)
        {
            _enemyType = type;
        }
    }

    public enum EnemyType
    {
        Fire,
        Water,
    }
}

This code allocates a new list every time it runs, which, when called every frame, leads to frequent memory allocations and GC overhead. Allocating memory each frame can trigger garbage collection more often, resulting in performance hiccups like frame drops.

Solution: Reuse Preallocated Lists

To avoid this, preallocate the list and clear it before each use:

using System.Collections.Generic;
using UnityEngine;

public class ListTest : MonoBehaviour
{
    private List<Enemy> _enemies = new List<Enemy>();
    
    // Preallocated list
    private List<Enemy> _matchingEnemies = new List<Enemy>(); 
    
    private void Start()
    {
        for (int i = 0; i < 1000; i++)
        {
            EnemyType enemyType = (EnemyType)Random.Range(0, 1);
            _enemies.Add(new Enemy(enemyType));
        }
    }

    private void Update()
    {
        FindEnemiesByType(EnemyType.Water);
    }

    private List<Enemy> FindEnemiesByType(EnemyType enemyType)
    {
        _matchingEnemies.Clear(); // Reuse the list
        for (int i = 0; i < _enemies.Count; i++)
        {
            if (_enemies[i].EnemyType == enemyType)
                _matchingEnemies.Add(_enemies[i]);
        }
        return _matchingEnemies;
    }

    public class Enemy
    {
        private EnemyType _enemyType;
        public EnemyType EnemyType => _enemyType;

        public Enemy(EnemyType type)
        {
            _enemyType = type;
        }
    }

    public enum EnemyType
    {
        Fire,
        Water,
    }
}

By clearing and reusing the preallocated list, we avoid creating new memory allocations every frame. This reduces the pressure on the garbage collector and minimizes performance spikes during gameplay.

Conclusion

Minimizing memory allocations, especially in frequently called methods, is essential for maintaining performance in Unity. Preallocating and reusing collections is a simple yet effective optimization that can prevent frame drops and GC spikes, leading to smoother gameplay and better user experiences.

By Rufi

Leave a Reply

Your email address will not be published. Required fields are marked *