Unity's compiler not great?

Hi all,
Wondering if this is a compiler problem or if someone can enlighten me further…

In the profiler I noticed that iterating through a list using foreach(…) was allocating 24 bytes of memory. As far as I know, List's enumerator is a struct, ie: stored on the stack… So I tried an empty loop to be sure, but it seems the foreach loop is still allocating 24 bytes… Got me thinking that maybe Unity’s compiler is playing tricks on me.

I suspect it could be compiling any foreach loop to a generic while loop that includes a finally block with an IDisposable cast which is where the 24 bytes is coming from… hmm. I’m not aware of a code reflector for OSX so I can’t confirm myself…

Any thoughts?

How do you know that 24 bytes allocation is on the heap and not on the stack?

Does it get unallocated once exiting the loop’s scope, or even exiting the method’s scope?

Is it allocated for each loop or only once entering the foreach scope?

What would the IDisposable actually dispose?

It’s showing up in the profiler in the GC allocations.

I know right, but technically List.Enumerator implements IDisposable…
If the compiler is breaking it down into a while loop —

try {
    while(...) {
        // ...
    }
}
finally {
    IDisposable disposable = iterator as IDisposable;
    if(disposable != null) disposable.Dispose();
}

I suspect it’s either the disposable in the finally block or my other thought is that maybe it’s boxing the List.Enumerator to a IEnumerator…

So, in both case, they should be garbage collected right after the foreach (in the next garbage collection cycle).

Enumerators are a class… whether it’s a standard Enumerator or an Enumerator… they implement IEnumerator. Any time your do a foreach loop you are going to allocate for the enumerator. It’s even worse if you have nested foreach loops. Let’s say you do this:

foreach(var obj in myObjects)
{
      foreach(innerObj in obj)
     {
           //do something
     }
}

In the example above we assume we have something like a IEnumerable<IEnumerable>. Now, in the above, you will allocate for the outer enumerator and for each object in “myObjects” you’ll also allocated another Enumerator.

For some types this can’t be avoided. But if you’re using a List, you should use your index instead:

for(var i = 0; i < myList.Count; i++)
{
      var obj = myList[i];
}

This indexed based example does not create an Enumerator and therefore will not allocate. This has nothing to do with Unity’s compiler, it has to do with the simple fact that you’re creating an instance of a reference type.

Then care to explain why this is not a class?

Yes, I’m very much aware of how both memory allocation and enumerators function (I say that informatively not indignantly :slight_smile: )

I’m sorry that’s incorrect. Most enumerators are reference type variables. In the case of a List it’s enumerator is actually a mutable struct. One of those oddities that works in our favor most times in terms of memory->performance. Which is why I posted this question - Because it should not be generating any garbage!

Now I’m wondering why it does… the only explanation I can think of is the compiler is being lazy as explained above and treating all loops the same, ie: foreach on a List like a IList, ie: boxing the returned enumerator to an IEnumerator

You’re right, I had to do a sanity check. List.Enumerator is a struct. I also checked the Mono source repository to make sure it was consistent and it is. List should not generate garbage in most cases. However, Collection will. If you foreach over a Dictionary for instance, you’ll generate garbage.

Now, I said “most” cases because it can. Take this example… this should not generate garbage in the foreach:

var myList = new List<int>();
myList.Add(1);
myList.Add(2); 

foreach(var myVal in myList)
{
     Debug.Log(myVal);
}

However, if you’ve cast back to an IEnumerable, which you may have unknowingly… then it will. Take this as an example:

public void Foo()
{
    var myList = new List<int>();
    myList.Add(1);
    myList.Add(2); 

    Bar(myList);
}

public void Bar(IEnumerable<int> myList)
{
    foreach(var myVal in myList)
    {
         Debug.Log(myVal);
    }
}

That example will actually generate garbage even though your original type was a List. I can run it through CLR Profiler to verify if you’d like. If we saw your actual code that you’re using, I bet it’s the case.

No funny code business going on here :slight_smile:
If anyone want’s to profile this in Unity for me to confirm, the results would be interesting…

using UnityEngine;
using System.Collections.Generic;


public class Test : MonoBehaviour 
{
	List<int> _list;

	void Start () 
	{
		_list = new List<int>();
		
		for(int i=0; i<10; i++) {
			_list.Add(1);
		}
	}
	
	void Update () 
	{	
		foreach(var element in _list) {
			
		}
	}
}

It will generate garbage if it is indeed the compiler doing what I think it’s doing…

I would like you to do it, because I don’t see why it wouldn’t call the proper implementation. You can implement your own IEnumerable and IEnumerator that have their own behaviour. I found out IEnumerator can implement IDisposable because you can make a IEnumerator that enumerate over marshallable data, such as a stream, which could be rather handy to automatically dispose of it after reading it!

List<>'s enumerator is List<>.Enumerator (nested class). It would break the behaviour if GetEnumerator() would return something else than an instance of this type.

In background, the foreach is suppose to be;

enumerator = stack.GetEnumerator();

try
{
    while (enumerator.MoveNext())
    {
        //statement
    }
}
finally
{
    IDisposable disposable = (enumerator as IDisposable);
    if (disposable != null)
        disposable.Dispose();
}

Looking at this, if the Enumerator if a struct, it shouldn’t allocate anything on the heap. Casting something into an interface (IDisposable) shouldn’t change the memory behaviour of a struct. At best, it should create a pointer on the stack.

From what I understand, casting it to IEnumerable boxes and unboxes the enumerator which is the problem. However, I didn’t see this behavior at all. The CLR Profiler won’t attach to the game executable for some reason… However I just threw in some quick GC code to check the allocated memory and ran against two tests. One with a List and the other with a List passed into a method that accepts IEnumerable as the parameter. In both cases there was zero allocation for the foreach iteration. You can test yourself:

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

public class ForEachTest : MonoBehaviour {

	public List<int> _list;

	private long mem;
	private long mem2;

	// Use this for initialization
	void Start () {
		_list = new List<int>{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	}
	
	// Update is called once per frame
	void Update ()
	{
		if (Input.GetKeyDown(KeyCode.A))
		{
			

			Test1(_list);

		}

		if (Input.GetKeyDown(KeyCode.B))
		{
			Test2(_list);
		}
	}

	private void Test1(List<int> myList)
	{
		mem = GC.GetTotalMemory(false);

		foreach (var num in myList)
		{
			continue;
		}

		mem2 = GC.GetTotalMemory(false);

		Debug.Log("A: " + mem + " / B: " + mem2);
	}

	private void Test2(IEnumerable<int> myList)
	{
		mem = GC.GetTotalMemory(false);

		foreach (var num in myList)
		{
			continue;
		}

		mem2 = GC.GetTotalMemory(false);

		Debug.Log("A: " + mem + " / B: " + mem2);
	}
}

Sums it up really :slight_smile:
After running some more tests this morning, I think it’s safe to say that the compiler is boxing the List.Enumerator to an IEnumerator.
Now just need a way of confirming…

Hmm interesting… I can confirm your results Dustin, no allocation. However, if I move the Test1 loop into the if statement, the memory allocation is back. Have to go out now, but will test more later :slight_smile:

Which “if statement” are you referring to? You mean the:

if(Input.GetKeyDown(KeyCode.A))
{
 //////
}

?? That’s the first test I ran before splitting it out and testing within methods so I could do the IEnumerable parameter. I still had zero allocation. You must have something else that’s allocating.

Yep.
For the sake of testing, can we keep the test in its most basic form like I posted above —

using UnityEngine;
using System.Collections.Generic;

public class Test : MonoBehaviour 
{
    List<int> _list;

    void Start () 
    {
        _list = new List<int>();

        for(int i=0; i<10; i++) {
            _list.Add(1);
        }
    }

    void Update () 
    {   
        foreach(var element in _list) {

        }
    }
}

All the other implementations work as expected and are self-explanatory. I’m only dealing with iterating over a vanilla List.
Just out of curiosity, which OS are you using? Windows/OSX? Visual Studio/Mono, etc.
The reason I ask is that any code compiled with VS should be fine…
I’m on OSX and I suspect it is a problem with the Mono compiler.

If anyone else is curious enough to test can they include their environment specs as well? :slight_smile:

Curious… I found this post —

So maybe I’m not going crazy after all! :stuck_out_tongue: Still, it would be nice to confirm it. Might have to load up a VM and see if I can get a reflector on it and see what’s actually happening…

dunno if its helpfull but here we go

.class public auto ansi beforefieldinit Test
	extends [UnityEngine]UnityEngine.MonoBehaviour
{
	// Fields
	.field private class [mscorlib]System.Collections.Generic.List`1<int32> _list

	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x20ec
		// Code size 7 (0x7)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
		IL_0006: ret
	} // end of method Test::.ctor

	.method private hidebysig 
		instance void Start () cil managed 
	{
		// Method begins at RVA 0x20f4
		// Code size 43 (0x2b)
		.maxstack 5
		.locals init (
			[0] int32
		)

		IL_0000: ldarg.0
		IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
		IL_0006: stfld class [mscorlib]System.Collections.Generic.List`1<int32> Test::_list
		IL_000b: ldc.i4.0
		IL_000c: stloc.0
		IL_000d: br IL_0022
		// loop start (head: IL_0022)
			IL_0012: ldarg.0
			IL_0013: ldfld class [mscorlib]System.Collections.Generic.List`1<int32> Test::_list
			IL_0018: ldc.i4.1
			IL_0019: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
			IL_001e: ldloc.0
			IL_001f: ldc.i4.1
			IL_0020: add
			IL_0021: stloc.0

			IL_0022: ldloc.0
			IL_0023: ldc.i4.s 10
			IL_0025: blt IL_0012
		// end loop

		IL_002a: ret
	} // end of method Test::Start

	.method private hidebysig 
		instance void Update () cil managed 
	{
		// Method begins at RVA 0x212c
		// Code size 55 (0x37)
		.maxstack 8
		.locals init (
			[0] int32,
			[1] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
		)

		IL_0000: ldarg.0
		IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<int32> Test::_list
		IL_0006: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
		IL_000b: stloc.1
		.try
		{
			IL_000c: br IL_0019
			// loop start (head: IL_0019)
				IL_0011: ldloca.s 1
				IL_0013: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
				IL_0018: stloc.0

				IL_0019: ldloca.s 1
				IL_001b: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
				IL_0020: brtrue IL_0011
			// end loop

			IL_0025: leave IL_0036
		} // end .try
		finally
		{
			IL_002a: ldloc.1
			IL_002b: box valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
			IL_0030: callvirt instance void [mscorlib]System.IDisposable::smile:ispose()
			IL_0035: endfinally
		} // end handler

		IL_0036: ret
	} // end of method Test::Update

} // end of class Test

compiled om Windows 7