Does foreach generate GC Alloc in Unity3D C#?

1. Will Foreach generate GC?

Many readers may hear that foreach traversing collections will generate GC while listening to the conversations of some bigwigs in the group, and the author also knows this, so many readers may see various statements on the Internet like the author, and are half-believing.
Mainly divided into these positions:

1.foreach will generate GC, don't use it in unity, Mono's problem
2.foreach generates GC because there is a problem with the collection being traversed, and the implementation is not good. It is not the fault of foreach
3. The GC problem of foreach has been fixed, and you can use it without any scruples

On the basis of searching the information, the author personally experimented, trying to prove which of these conclusions is correct, and the conclusion obtained is

Other answers on the Internet are too ancient, and there is even inexplicable discrimination foreach
Sometimes there will be a little bit of GC, but there is no need to negate, and it is even negligible now

If you don’t want to read the experiment process, you can directly turn to the end of the article for the conclusion! ! ! !

2. Experimental process

First of all, let's discuss with the most commonly used Dictionary, because we often use foreach to traverse Dictionary conveniently, which is difficult to use for

1. foreach traversal dictionary exists GC

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
public class MyGCTest : MonoBehaviour
{
    Dictionary<int,int> dic = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 1, 1 }
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        foreach (var x in dic){}
        Profiler.EndSample();
    }
}


The answer obviously exists, but the author inadvertently discovered that the foreach written in Update actually only generates GC when it is called for the first time, and the foreach in subsequent cycles does not generate GC! !

2. When does foreach traverse the dictionary to generate GC Alloc

According to the previous step, the author has the following conjectures

1. Add an element to the dictionary, will foreach generate GC again?
2. If I traverse multiple dictionaries separately, will there be double GC
3. What if I iterate over several dictionaries of different types?

According to the following code verification, the author monitors the generation of GC in the two files respectively

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;


public class MyGCTest : MonoBehaviour
{
	//First complete two different dictionaries
    Dictionary<int, int> dic = new Dictionary<int, int>()
    {
        { 0, 0 }
    };
    Dictionary<int, int> dic2 = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 100,100}
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        //Try traversing the first dictionary first
        foreach (var x in dic){}
        //Add an element and try again
        if (!dic.ContainsKey(1)) dic.Add(1, 0);
        foreach (var x in dic){}
        //Iterate over the second dictionary
        foreach (var x in dic2) {}
        Profiler.EndSample();
        //At this time, it is found that the first frame has 0B GCAlloc
    }
}
public class MyGCtest2 : MonoBehaviour
{
    Dictionary<int, int> dic2 = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 100,100}
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC3");
        foreach (var x in dic2) {}
        Profiler.EndSample();
        //The first frame produces 96B GCAlloc
    }
}



Thus we can derive

1. Regardless of traversing several dictionaries, traversing several times, and whether elements change, only 96B GCAlloc is generated
2. The GCAlloc of the foreach traversal dictionary is only generated once globally, and has nothing to do with the file, method, or class where it is located

But we want to try different types of dictionaries next...

Dictionary<int, int> dic2 = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 100,100}
    };

    Dictionary<int, float> dic1 = new Dictionary<int, float>()
    {
        { 0, 0.2f },
        { 100,100.0f}
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        foreach(var x in dic2)
        {

        }
        foreach (var x in dic1)
        {

        }
        Profiler.EndSample();
    }


The GC suddenly becomes 192B, which is twice the original, and obviously each type of dictionary will generate 96B

1. Regardless of traversing several dictionaries, traversing several times, and whether elements change, only 96B GCAlloc is generated
2. The GCAlloc of the foreach traversal dictionary is only generated once globally, and has nothing to do with the file, method, or class where it is located
3. The GC generated by foreach traversing the dictionary is related to the dictionary type

3. What about foreach traversing other Collection s?

	List<int> list = new List<int>() { 0,1,0};
    int[] arr= new int[3] { 0,1,0};
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        foreach(var x in list)
        {

        }
        foreach(var x in arr) { }
        Profiler.EndSample();
    }

Even the first GC didn't happen, so let's dig out the principle carefully.

3. Why does foreach generate GC when traversing Dictionary

The essence of foreach is the simplification of methods such as GetEnumerator() and MoveNext(). We are very familiar with interfaces such as IEnumerable.

1. Regardless of traversing several dictionaries, traversing several times, and whether elements change, only 96B GCAlloc is generated
2. The GCAlloc of the foreach traversal dictionary is only generated once globally, and has nothing to do with the file, method, or class where it is located
3. The GC generated by foreach traversing the dictionary is related to the dictionary type

These conclusions are generated, and it is concluded that the CG output of foreach is related to the dictionary type, but not related to other factors. I can guess that GetEnumerator always returns a single instance of Enumerator, and each dictionary type contains an instance, so each dictionary type will generate a certain GC phenomenon.



We analyzed in detail and found that 96B is generated at GetEnumerator and 96B is generated at MoveNext
Even in order to explore this content, the author wrote a third Dictionary and found that GetEnumerator generated 144B and MoveNext generated 144B
The author came to the following conclusions

1. The first foreach of each type Dictionary<T,K> generates 96B GCAlloc
2. Each 96B GCAlloc is 48B GetEnumerator() and 48B MoveNext()
3. The iteration method of Dictionary is similar to singleton, and each type is loaded only once globally

4. Conclusion

  1. foreach will not generate unacceptable GC for no reason when traversing the collection in System.Collections.Generic
    In fact, Enumerator will not be created when traversing List and array, that is, 0GC will always be maintained
  2. When foreach traverses the dictionary, it only generates a GC for each type of dictionary when it is called for the first time, and no GC will be generated for the same type of dictionary in the future, regardless of other factors.
    That is to say, you only need to use foreach on Dictionary<int,int>, and then use foreach of the same type of dictionary in the future will not generate GC, no matter whether it is the same instance, whether the element changes, whether the file is the same, whether the method and class are same.
    3. Separate foreach for the dictionary Values/Keys will generate more GC, about 24B more, which is similar to the above mentioned, and the others are the same.

Tags: C# architecture Unity programming language

Posted by jcvertin on Thu, 26 Jan 2023 00:21:45 +0530