Thursday, June 11, 2015

Unity3D Micro Optimisation: Loops

This benchmark answers two questions.

1. What is the performance difference between different loop statements? The two options considered are "for" and "foreach". Why bother? These things matter when dealing with modifying 4000 particles per frame in your latest Unity game on a mobile device.

2. What is the performance difference between different collections? Arrays and Generic lists are commonly used in Unity, and I've always wondered about the performance difference of accessing arrays by index, vs lists by index.

The results are quite surprising. Benchmark code below, but here are my results:

  • For Each Loop over Array: 262 ticks
  • For Loop over Array: 120 ticks
  • For Each Loop over List: 1640 ticks
  • For Loop over List: 598 ticks

The (expected) winner is clearly a plain for loop over an array, but I didn't realise the difference between the loop structures and collections would be so large!


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

public class LoopBenchmarks : MonoBehaviour
{
    public int iterations = 15;
    public int arraySize = 40;

    void Start ()
    {
        BenchArray ();
        BenchList ();
    }

    void BenchArray ()
    {
        var array = new int[arraySize];
        var clock = new Stopwatch ();
        var foreachTime = 0f;
        var forTime = 0f;
        var mapTime = 0f;
        for (var iteration=0iteration<iterationsiteration++) {
            clock.Start ();
            foreach (var i in array) {
            }
            clock.Stop ();
            foreachTime += clock.ElapsedTicks;
            clock.Reset ();
            clock.Start ();
            for (var idx=0idx<array.Lengthidx++) {
                var i = array [idx];
            }
            clock.Stop ();
            forTime += clock.ElapsedTicks;
            clock.Reset ();
        }
        UnityEngine.Debug.Log (string.Format ("For Each Loop over Array: {0} ticks"foreachTime / iterations));
        UnityEngine.Debug.Log (string.Format ("For Loop over Array: {0} ticks"forTime / iterations));
    }

    void BenchList ()
    {
        var list = new List<int> (new int[arraySize]);
        var clock = new Stopwatch ();
        var foreachTime = 0f;
        var forTime = 0f;
        for (var iteration=0iteration<iterationsiteration++) {
            clock.Start ();
            foreach (var i in list) {
            }
            clock.Stop ();
            foreachTime += clock.ElapsedTicks;
            clock.Reset ();
            clock.Start ();
            for (var idx=0idx<list.Countidx++) {
                var i = list [idx];
            }
            clock.Stop ();
            forTime += clock.ElapsedTicks;
            clock.Reset ();
        }
        UnityEngine.Debug.Log (string.Format ("For Each Loop over List: {0} ticks"foreachTime / iterations));
        UnityEngine.Debug.Log (string.Format ("For Loop over List: {0} ticks"forTime / iterations));
    }
    
}


Update: Thanks to a comment below, I've added in a new benchmark, and run the test in the latest Unity5.1; The results are surprising. Surprising enough that I added some work to each benchmark just to be sure the compiler wasn't optimising away the useless loops.

  • For Each Loop over Array: 184 ticks
  • For Loop over Array: 206 ticks
  • For Loop over Array with cached Length: 201 ticks
  • For Each Loop over List: 1153 ticks
  • For Loop over List: 686 ticks

It seems that the foreach loop over an array is now FASTER than a for loop. The other surprise is that caching the Length attribute has a very small impact on performance. These results were unexpected... if I get time I'll do some more tests to confirm.

This is the new Benchmark code.

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

public class LoopBenchmarks : MonoBehaviour
{
  public int iterations = 1500;
  public int arraySize = 4000;
  
  void Start ()
  {
    BenchArray ();
    BenchArrayWithLengthCached ();
    BenchList ();
  }
  
  void BenchArray ()
  {
    var array = new int[arraySize];
    var clock = new Stopwatch ();
    var foreachTime = 0f;
    var forTime = 0f;
    for (var iteration=0; iteration<iterations; iteration++) {
      clock.Start ();
      foreach (var i in array) {
        if (i < 0)
          throw new System.Exception ();
      }
      clock.Stop ();
      foreachTime += clock.ElapsedTicks;
      clock.Reset ();
      clock.Start ();
      for (var idx=0; idx<array.Length; idx++) {
        var i = array [idx];
        if (i < 0)
          throw new System.Exception ();
      }
      clock.Stop ();
      forTime += clock.ElapsedTicks;
      clock.Reset ();
    }
    UnityEngine.Debug.Log (string.Format ("For Each Loop over Array: {0} ticks", foreachTime / iterations));
    UnityEngine.Debug.Log (string.Format ("For Loop over Array: {0} ticks", forTime / iterations));
  }

  void BenchArrayWithLengthCached ()
  {
    var array = new int[arraySize];
    var clock = new Stopwatch ();

    var forTime = 0f;

    for (var iteration=0; iteration<iterations; iteration++) {
      int length = array.Length;
      clock.Start ();
      for (var idx=0; idx<length; idx++) {
        var i = array [idx];
        if (i < 0)
          throw new System.Exception ();
      }
      clock.Stop ();
      forTime += clock.ElapsedTicks;
      clock.Reset ();
    }
    UnityEngine.Debug.Log (string.Format ("For Loop over Array with Cached Length: {0} ticks", forTime / iterations));
  }
  
  void BenchList ()
  {
    var list = new List<int> (new int[arraySize]);
    var clock = new Stopwatch ();
    var foreachTime = 0f;
    var forTime = 0f;
    for (var iteration=0; iteration<iterations; iteration++) {
      clock.Start ();
      foreach (var i in list) {
        if (i < 0)
          throw new System.Exception ();
      }
      clock.Stop ();
      foreachTime += clock.ElapsedTicks;
      clock.Reset ();
      clock.Start ();
      for (var idx=0; idx<list.Count; idx++) {
        var i = list [idx];
        if (i < 0)
          throw new System.Exception ();
      }
      clock.Stop ();
      forTime += clock.ElapsedTicks;
      clock.Reset ();
    }
    UnityEngine.Debug.Log (string.Format ("For Each Loop over List: {0} ticks", foreachTime / iterations));
    UnityEngine.Debug.Log (string.Format ("For Loop over List: {0} ticks", forTime / iterations));
  }
  
}

Thursday, April 09, 2015

Malloc in BinaryReader and BinaryWriter

In my efforts to write a malloc-free network library for Unity3D, I discovered that BinaryReader and BinaryWriter perform malloc operations when reading/writing floats and doubles.

It seems this is the case, because C# does not allow bit shifting operations on these types, which is used to turn these types into 4 or 8 bytes.

I ended up using this code to convert these types without using the builtin BitConverter (which performs a malloc!)


float[] FLOAT = new float[1];

public float ReadFloat ()
{
   Buffer.BlockCopy(buffer, (int)readStream.Position, FLOAT, 0, 4);
   readStream.Position += 4;
   return FLOAT[0];
}

public void Write (float value)
{
   FLOAT[0] = value;
   Buffer.BlockCopy(FLOAT, 0, buffer, (int)writeStream.Position, 4);
   writeStream.Position += 4;
}


Thursday, November 13, 2014

Python-3 Game Server

I've been working on something new. I get a lot of projects where I have to add multiuser functionality. Finally I've made the jump to Python 3, and built a generic multi-user game server, so I can stop rewriting this sort of thing for every new project. It's using the most excellent asyncio framework, with aiopg for Postgresql support.

The server code is on GitHub, the Unity3D client is still in progress.

Those tedious functions, such as Registration, Authentication, Reset Password, Messaging, Rooms and Object storage are solved, may I never have to write a "Reset Password" module again!

Thursday, October 09, 2014

4 Million Vertices of Procedural Spacecraft


Still lots of improvements to be made... but it is definitely getting better.

Popular Posts