Every iteration of every “foreach” loop generated 24 Bytes of garbage memory?

13,292

Solution 1

foreach is an interesting beast; a lot of people mistakenly think it is tied to IEnumerable[<T>] / IEnumerator[<T>], but that simply isn't the case. As such, it is meaningless to say:

Every iteration of every “foreach” loop generated 24 Bytes of garbage memory.

In particular, it depends on exactly what you are looping over. For example, a lot of types have custom iterators - List<T>, for example - has a struct-based iterator. The following allocates zero objects:

// assume someList is a List<SomeType>
foreach(var item in someList) {
   // do something with item
}

However, this one does object allocations:

IList<SomeType> data = someList;
foreach(var item in data) {
   // do something with item
}

Which looks identical, but isn't - by changing the foreach to iterate over IList<SomeType> (which doesn't have a custom GetEnumerator() method, but which implements IEnumerable<SomeType>) we are forcing it to use the IEnumerable[<T>] / IEnumerator<T> API, which much by necessity involve an object.

Edit caveat: note I am assuming here that unity can preserve the value-type semantics of the custom iterator; if unity is forced to elevate structs to objects, then frankly all bets are off.

As it happens, I'd be happy to say "sure, use for and an indexer in that case" if every allocation matters, but fundamentally: a blanket statement on foreach is unhelpful and misleading.

Solution 2

Your friend is correct, the following code will generate 24k of garbage every frame in Unity as of version 4.3.4:

using UnityEngine;
using System.Collections.Generic;

public class ForeachTest : MonoBehaviour 
{
    private string _testString = "this is a test";
    private List<string> _testList;
    private const int _numIterations = 10000;

    void Start()
    {
        _testList = new List<string>();
        for(int i = 0; i < _numIterations; ++i)
        {
            _testList.Add(_testString);
        }
    }

    void Update()
    {
        ForeachIter();
    }

    private void ForeachIter()
    {
        string s;

        for(int i = 0; i < 1000; ++i)
        {
            foreach(string str in _testList)
            {
                s = str;
            }
        }
    }
}

This shows the allocation in the Unity profiler:

Unity Profiler showing 24k of allocation per update

According to the following link it's a bug in the version of mono being used by Unity that forces the struct enumerator to get boxed, which seems like a reasonable explanation though I haven't verified through code inspection: Blog post discussing the boxing bug

Solution 3

There is some good advice on here about foreach, but I must chime in about the Mark's comment above relating to tags (unfortunately, I don't have enough rep to comment below his comment), where he stated,

Actually, I have to take that article with a pinch of salt, because it claims "Calling the tag property on an object allocates and copies additional memory" - where tag here is a string - sorry, but that simply isn't true - all that happens is that the reference to the existing string object is copied on the stack - there is no extra object allocation here. –

Actually, calling the tag property does create garbage. My guess is that internally the tag's are stored as integers (or some other simple type), and when the tag property is called, Unity converts the int to a string using an internal table.

It defies reason, since the tag property could just as easily return the string from that internal table rather than creating a new string, but for whatever reason, this is how it works.

You can test this yourself using the following simple component:

public class TagGarbageTest : MonoBehaviour
{
    public int iterations = 1000;
    string s;
    void Start()
    {
        for (int i = 0; i < iterations; i++)
            s = gameObject.tag;
    }
}

If gameObject.tag was not producing garbage, then increasing/decreasing the number of iterations would have no effect on the GC Allocation (in the Unity Profiler). In fact, we see a linear increase in GC Allocation as the number of iterations increases.

It is also worth noting that the name property behaves in exactly the same manner (again, the reason why eludes me).

Share:
13,292
jimpanzer
Author by

jimpanzer

contact me: mail

Updated on July 22, 2022

Comments

  • jimpanzer
    jimpanzer almost 2 years

    My friend works with Unity3D on C#. And he told me specifically:

    Every iteration of every “foreach” loop generated 24 Bytes of garbage memory.

    And I also see this information here

    But is this true?

    The paragraph most relevant to my question:

    Replace the “foreach” loops with simple “for” loops. For some reason, every iteration of every “foreach” loop generated 24 Bytes of garbage memory. A simple loop iterating 10 times left 240 Bytes of memory ready to be collected which was just unacceptable

  • Gary Walker
    Gary Walker over 10 years
    I would also add that the "Garbage Memory" is not memory that will not be gargage collected. Rather it is memory that is no longer used and will be garbage collected. Games development often has a different focus that typical applications in terms of performance and overhead.
  • Chris Blackwell
    Chris Blackwell about 10 years
    Unfortunately this seems to be incorrect in Unity (possible with Mono in general?) I've added another comment with example code that demonstrates the garbage being created. Every time foreach is called, 24 bytes are allocated.
  • hangar
    hangar over 9 years
    It used to happen in .NET as well. It's not the GetEnumerator() call, but the Dispose(). See this.
  • Selmar
    Selmar over 8 years
    I repeated this test in Unity 5.2.0.f3 and I found that it generates ~39.1 KB each frame (which is 40 bytes per iteration).
  • yzt
    yzt over 8 years
    You say "It's meaningless to say...", and it would have been, unless that's exactly what happens. Using C# and Unity3D 4.x (which uses Mono), every iteration of every foreach loop that I've observed creates 24 bytes of garbage to be collected.
  • Marc Gravell
    Marc Gravell over 8 years
    @yzt and again; without details of what you are looping over, and what things happen inside the loop (closures, etc), I assert: it is meaningless. If you are talking about a specific scenario, then sure: you can start to talk about meaning. Context is everything here.
  • Søren Løvborg
    Søren Løvborg almost 6 years
    This is fixed in recent Unity versions (5.5+): q.unity3d.com/questions/1465/…