Resource management in Unity - reference count

This article shares resource management in Unity - reference counting

In the previous article, we learned the basic principle and several implementations of object pool. Today, we continue to talk about another important technology in resource management: reference counting

Basics of GC

GC(Garbage Collection) is a scheme used to automatically manage memory. Developers do not need to worry too much about resource release. Many languages (such as C#, Java) have their own GC

We know that memory allocation can come from the stack and heap. Generally, stack space follows the life cycle of the function. It is applied during the use of the function and released after the use of the function. Generally, local variables (non new keyword applications) applied within the function will be destroyed after the execution of the function The so - called function stack refers to the function stack space, and each function is independent

The life cycle of the memory from the heap space application (generally new keyword application) will exceed the function, or even exceed the class, which is shared by the whole program This kind of memory is theoretically infinite (affected by program memory), requiring developers to apply and release themselves (because it crosses the scope)

In languages without GC, such as C/C + +, developers need to release memory in a suitable place, otherwise memory leakage will occur (a piece of memory is no longer used, but cannot be released). Using these languages needs to be very careful, so the threshold of these languages is generally higher than that of languages with GC I believe you have been tortured by concepts such as pointer and quotation

In the language of GC, heap memory is further divided into managed heap and unmanaged heap As the name suggests, managed heap is managed and maintained by GC, while unmanaged heap is managed by developers (such as file references)

With GC, the banner of memory release is carried by GC. In most cases, developers no longer worry about memory release, which greatly reduces the development threshold and allows developers to focus more on business without having to be careful about memory problems all the time

Of course, there are advantages and disadvantages, otherwise C/C + + would have been eliminated

The biggest disadvantage of a language with GC lies in GC itself

First of all, GC itself needs to occupy a certain amount of memory and consume a certain amount of performance. It can't give full play to its performance like C language That is, languages with GC are naturally larger, more complex and slower than languages without GC

Secondly, GC recovery is not very timely. Although manual intervention can be carried out, it will also lack great flexibility and timeliness

Finally, developers who are used to GC always have great trust in GC and are used to flying themselves, so they may not use memory correctly and cause memory leakage

What is reference counting

Although most memory can be managed, we don't need to worry about it, we still need to maintain unmanaged memory, such as various resources used in the game, texture, sound, prefab, etc

The most common solution is reference counting

In short, reference counting is to maintain a count for each reference of the object. Releasing references only reduces the count by one. The real object destruction is performed only when the count is 0

In the development of game client, various resources actually occupy a large amount of memory, and the resources are limited, and the cost of applying, initializing and destroying resources is generally expensive. Therefore, more reference counts will be used to manage these memory and apply, use and destroy them when they are really needed

Principle of reference counting

Like the object pool, the principle of reference counting is very simple, but it is not easy to use it well. In short, who applies for who releases, and the application and release are used in pairs

Next, we define a reference counting interface and class and attach a simple test case:

public interface IRefCounter {
    // Reference count
    int refCount {get;}

    // Increase Holdings
    void Retain();

    // reduce one's shares in a listed company
    void Release();

    // Operation when is zero
    void OnRefZero();
}

public class RefCounter : IRefCounter {
    public int refCount {get; private set;}

    // Plus 1
    public void Retain() {
        refCount++;
    }
    
    // Minus 1
    public void Release() {
        refCount--;

        if (refCount == 0) {
            OnRefZero();
        }
    }

    public virtual void OnRefZero() {
		Debug.Log("Release resources")
    }
}

// In and out of the room, the first to enter the room and open the door, and the last to leave the room and close the door
public class Door : RefCounter {
    public void SwitchOn() {
        Retain();

        if (refCount > 1) return;
        Debug.Log("First in the room, Open the door.");
    }

    public void SwitchOff() {
        Release();
    }

    public override void OnRefZero() {
        Debug.Log("The last one to leave the room, close.");
    }
}

public class Person {
    private Door m_Door;

    // Entering the room is equivalent to applying for memory and holding (count + +)
    public void EnterDoor(Door door) {
        m_Door = door;

        door.SwitchOn();
    }

    // Leaving the room is equivalent to releasing memory and reducing Holdings (count --)
    public void LeaveDoor() {
        Assert.IsNotNull(m_Door, "Advanced room required!");

        m_Door.SwitchOff();
        m_Door = null;
    }
}

public class RefCounterTest : MonoBehaviour {
    public Button btnOpen;
    public Button btnClose;

    private List<Person> m_AllPerson = new List<Person>();

    private void Awake() {
        var door = new Door();
        
        // Every time you enter, open the door. Only the first one is the real door
        btnOpen.onClick.AddListener(() => {
            var person = new Person();
            person.EnterDoor(door);

            m_AllPerson.Add(person);
        });

        // Every time you quit, close the door once, and only the last one really closes
        btnClose.onClick.AddListener(() => {
            var person = m_AllPerson.FirstOrDefault();
            if (person == null)
                return;

            person.LeaveDoor();
            m_AllPerson.Remove(person);
        });
    }

    private void OnDestroy() {
        foreach(var person in m_AllPerson) {
            person.LeaveDoor();
        }

        m_AllPerson.Clear();
    }
}

The code is relatively simple, that is, everyone counts when they enter the door and closes the door after everyone goes out

Automatic recycling

Imagine that our door is built in the game < < my world > >. After everyone leaves, we should not only close the door, but also destroy the whole door and recycle materials. We only need to add resource recycling when closing the door in the above implementation

But there is still a problem here. What if no one goes in after the door is built? When do we recycle?

Here, the so-called auto release pool can be used. After each door is created, in order to recycle at last, it is handed over to a manager and released at the end of the frame. If someone enters the door before that, it will not be recycled, but will be delayed until everyone goes out. If no one enters the door before the end of the frame, it will be released directly It's troublesome to say, but super simple to implement:

public class AutoReleasePool : Singleton<AutoReleasePool> {
    private List<IRefCounter> m_AutoReleaseCounter = new List<IRefCounter>();

    public void AddCounter(IRefCounter counter) {
        m_AutoReleaseCounter.Add(counter);
    }

    public void AutoRelease() {
        foreach(var counter in m_AutoReleaseCounter) {
            counter.Release();
        }
        
        m_AutoReleaseCounter.Clear();
    }
}

public interface IRefCounter {
    //.....

    // Automatic release
    void AutoRelease();
}

public class RefCounter : IRefCounter {
    //.....
    
    // Automatic release
    public void AutoRelease() {
        AutoReleasePool.instance.AddCounter(this);
    }
}

public class Door : RefCounter {
    //.....
    
    public override void OnRefZero() {
        Debug.Log("Close and destroy, Recycling resources.");
    }
}

public class RefCounterTest : MonoBehaviour {
    //.....
    
    private void Awake() {
        var door = new Door();
        door.AutoRelease();

        //.....
    }

    //.....
    private void LateUpdate() {
        AutoReleasePool.instance.AutoRelease();
    }
}

At the end of each frame, the holdings are reduced. If there are no other holders, the recovery will be triggered If it is used before the end of the frame, it will only be recycled in the automatic release pool

Automatic release pool is equivalent to an insurance, which avoids the problem of applying but not actually using

summary

The principle and implementation of reference counting are very simple, but there are a variety of use methods. There are different variants in different situations, but there is always one core: who applies for who releases, and the application and release are used in pairs As long as we firmly grasp this core, no matter how complex the implementation is, it can be easily understood

Today, we introduce the basic concept of reference counting and provide a simple example. Finally, we add an automatic release pool as insurance, which is enough to deal with most application scenarios

The current content is the precondition of resource management. In the next few articles, we will officially enter the content of resource management in Unity. Combined with Unity's official methods and these pre theories, we can form a complete set of resource management schemes that can be used for large-scale commercial projects I hope interested students will continue to pay attention

Well, that's all for today. I hope it will help you

Keywords: Unity

Added by AKalair on Thu, 23 Dec 2021 18:10:35 +0200