Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Foreach massively slower than for, when looping through vertices?

Discussion in 'Editor & General Support' started by jc_lvngstn, Mar 19, 2014.

  1. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    [Edit] When I posted, I got foreach/for mixed up, I meant to say that the foreach is much FASTER. Regardless, there is still a huge performance difference.

    I noticed something odd in my main project, so I created a very simple test scene. All it does is have a script which allows you to loop through the vertices in a mesh. That's it. You can toggle between a for loop, or a foreach.

    The top part of the screenshot show me using the for loop. The bottom shows the foreach loop.

    $foreach_perf.jpg


    The foreach performance is massively faster. The difference between 4fps using the for, and 2000+fps using the foreach loop.
    Here is the script:

    Code (csharp):
    1. public class ForTest : MonoBehaviour
    2. {
    3.     public long Milliseconds;
    4.     public bool UseForEach;
    5.     public Mesh TestMesh;
    6.     Stopwatch watch = new Stopwatch();
    7.     // Use this for initialization
    8.     private void Start()
    9.     {
    10.     }
    11.  
    12.     // Update is called once per frame
    13.     private void Update()
    14.     {
    15.         watch.Reset();
    16.         watch.Start();
    17.         if (UseForEach)
    18.         {
    19.             foreach (var vertex in TestMesh.vertices)
    20.             {
    21.                 Vector3 j = vertex;
    22.             }
    23.         }
    24.         else
    25.         {
    26.             for (int index = 0; index < TestMesh.vertices.Length; index++)
    27.             {
    28.                 var vertex = TestMesh.vertices[index];
    29.                 Vector3 j = vertex;
    30.             }
    31.         }
    32.  
    33.         watch.Stop();
    34.         Milliseconds = watch.ElapsedMilliseconds;
    35.     }
    36. }
    The mesh I use in the sample project has about 2k vertices. Nothing major at all.

    Here is the sample project:

    https://dl.dropboxusercontent.com/u/174731243/ForeachTest/ForeachTest.unitypackage

    It's very small...all it really is is this script attached to a en empty, and the set in the inspector. If you run it, just check the TestGo gameobject.

    I am using Windows 7, 64 bit.

    Anyway...would someone please check this and see if I am crazy, blind, or just need to call it a night.
     

    Attached Files:

    Last edited: Mar 19, 2014
  2. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,649
    You're blind; according to those screenshots you're getting the very high framerate when "Use For Each" is turned on, not off. It's your for loop that is slow, not the foreach.

    My guess as to why: every iteration of the 'for' loop, you're retrieving TestMesh.vertices twice, once to read a vertex from it and once to check its length. Each retrieval involves allocating a buffer for, and making a copy of, the entire vertex data. If you save TestMesh.vertices to a temporary variable before entering the loop and then access that temporary instead of accessing TestMesh.vertices directly, you should see pretty much equivalent performance.
     
  3. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    My goof, I was in such a hurry to post I got them switched around. I'll fix the post.

    As far as the deal about checking the length...wouldn't that happen anyway in the foreach loop? I would think that internally...it would still have to retrieve that value.
    Besides...it would really, really surprise me if just checking the length caused that much slowdown. I'll test it though.
     
  4. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    Ok. I cached the vertices in a variable, in Start(). That explains it.

    Unity is actually making a copy of Mesh.vertices when you access the property. I never noticed this until I switched from foreach to for. I probably have never used for to access a mesh's vertices, which does not use the enumerator.
    Thanks!
     
    Last edited: Mar 19, 2014
  5. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,649
    Yeah, foreach over an array is handled specially by the compiler. See lines 5228 through 5275 here; in particular, line 5232 saves a copy of whatever you're foreaching-over into a temporary variable, and then lines 5246-5252 set up integer counters to iterate over the entries in the array. The result is that

    Code (csharp):
    1.  
    2. foreach(var vert in Mesh.vertices)
    3. {
    4.    Debug.Log(vert);
    5. }
    6.  
    compiles to pretty much the exact same code as

    Code (csharp):
    1.  
    2. var t0 = Mesh.vertices;
    3. var t1 = t0.Length;
    4. for(int i = 0; i < t1; i++)
    5. {
    6.    var vert = t0[i];
    7.    Debug.Log(vert);
    8. }
    9.  
    Note that this does not happen for other collection types - only for arrays. Other collections use actual IEnumerator instances, which presently always incur a GC-able allocation due to a compiler bug.
     
  6. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,649
    Also, rule of thumb: every Unity API point that gives you an array of data, is giving you a copy. You're incurring both time spent allocating memory for the copy, and time spent doing the copying. The only exceptions are methods where you pass in the array to have Unity fill it for you, such as some of the Physics2D methods.
     
  7. Brainswitch

    Brainswitch

    Joined:
    Apr 24, 2013
    Posts:
    270
    Yeah, which is why it would be pretty nice if more (say Physics, and sure Mesh as well) classes got NonAlloc versions.