Does foreach in Unity allocate memory?
In the company I work at, we have a strict rule to follow: don’t use foreach
if you don’t have to. That means that instead iterating arrays (and lists) like this:
foreach (int n in myArray)
{
}
we do it using a for
loop:
for (int i = 0; i < myArray.Length; ++i)
{
int n = myArray[i];
}
Back in the days of Unity 5, there was a bug in Unity’s compiler implementation, that caused foreach
loop to allocate memory. Recently I was wondering: it has been over 6 years, 5 major Unity versions and a bunch of compiler improvements since then, so things has to be better now, right? Perhaps we can now get away from old superstitions and start using this sweet syntaxic sugar that has been around since forever at this point? So I made a little research to find that out, and learnt a few things along the way.
TL;DR
foreach
in general (not only in Unity) doesn’t allocate when used directly on collection objects. This is possible because all standard collections implement a specialGetEnumerator()
method which returns a struct, and this is used by theforeach
loop. For example, this code doesn’t allocate memory:
List<int> list = new List<int>();
foreach (int n in list)
{
//Do something
}
foreach
does allocate when used on collections via their interfaces likeICollection
,IList
, etc. This happens because the compiler doesn’t know the actual type of the variable and callsGetEnumerator()
from theIEnumerable
interface. This method returns a reference-type object, which means it will allocate memory. So this code is not GC-free:
IList<int> list = new List<int>();
foreach (int n in list) // allocates about 40B
{
//Do something
}
- While in some cases
foreach
doesn’t have memory overhead overfor
loop, it still slower thanfor
because of the added complexity of iterators. So technically it’s still better to usefor
when possible.
The experiment
So to check memory allocations I did a simple thing: made a small script where I initialized a few common collections and iterated over them with foreach
. I then ran this script through the Unity’s profiler and looked at the GC alloc
column. Unity version used is 2020.3.10f. You can find the whole project used for this on my Github.
Results looked like this:
So, no allocations? Woohoo! The bug is fixed, and we don’t have to worry about allocations anymore! But once the initial excitement cooled down and I thought about it a bit, I wondered: how come?
foreach and collections in .NET
The thing that made me wonder is the fact that foreach
in .NET acts on types that implement the IEnumerable<T>
interface 1. It exposes a single method: IEnumerator<T> GetEnumerator()
, whilst IEnumerator<T>
looks as following:
public interface IEnumerator<T> : IDisposable
{
T Current { get; }
bool MoveNext();
void Reset();
}
So under the hood, any foreach
statement expands more or less to this:
IEnumerator<T> enumerator = myCollection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
T val = enumerator.Current;
//foreach body goes here
}
}
finally
{
enumerator.Dispose();
}
My concern here was that since GetEnumerator()
returns an instance of an interface, it must allocate memory. Even if the implementation returns an instance of a value-type, due to boxing it will still be put on the heap. However, it turns out that it doesn’t have to be that way.
In MSDN article about foreach
we can find this paragraph:
The foreach statement isn’t limited to those types. You can use it with an instance of any type that satisfies the following conditions:
- A type has the public parameterless GetEnumerator method. Beginning with C# 9.0, the GetEnumerator method can be a type’s extension method.
- The return type of the GetEnumerator method has the public Current property and the public parameterless MoveNext method whose return type is bool.
Note that the return type of GetEnumerator()
doesn’t have to implement IEnumerator
. That means that it’s possible to declare a GetEnumerator()
that will return a struct and thus won’t allocate in foreach
. In fact, that’s exactly what List<T>
and other standard generic collections do. Here is the snippet from the List<T>
class:
public List<T>.Enumerator GetEnumerator();
//List<T>.Enumerator
public struct Enumerator : IEnumerator<T>, IEnumerator, IDisposable { //snip }
So to summarize, the ability to use value-types for GetEnumerator()
allows us not to allocate any memory while iterating with foreach
. There is one thing to remember though.
Be careful with interfaces
While the compiler is able to find the right GetEnumerator()
method in List
or HashSet
, it doesn’t have much choice when given an object of type IList
or ICollection
. When it sees an interface, its only option is to use GetEnumerator()
from the IEnumerable
interface, which all of these interfaces implement. This method returns an object of type IEnumerator
which is reference-type, therefore it will be allocated on heap regardless of the actual implementation of the interface.
Let’s consider an example. Let’s say we create an instance of List<int>
and pass it to another method to calculate the average. But the Average
method takes IReadOnlyList<int>
as a parameter, to provide some degree of abstraction.
List<int> list = new List<int> { 4, 24, 13, 42 };
float average = Average(list);
private float Average (IReadOnlyList<int> numbers)
{
float average = 0;
//Here IEnumerator<int> IEnumerable<int>.GetEnumerator() will be used
//allocating memory on the heap
foreach (int n in numbers)
{
average += n;
}
return average / numbers.Count;
}
Because of this if we really want to use foreach on collections, we need to first make sure that the one we work with is actually of a concrete type, not an interface. And if this seems doable, it’s impossible to make sure that another person working on this piece of code tomorrow won’t change the type to an interface and you will not get the additional GC allocation. Thus, while it’s sometimes tempting to save a few seconds of typing and write a foreach
, I’d still recommend sticking to for
wherever possible. This is just more robust and reliable solution, which also is more performant in terms of CPU.
-
In this article I focus on generic collections like
List<T>
andDictionary<TKey, TValue>
. Their non-generic versions work in similar way. ↩︎