Code Optimization Practices

Preamble

Code optimization is very important for developers; especially video game developers. In a world where reflexes and timing are so important you have to keep optimization always in mind. There are good practices and bad practices for optimizing your code.

In providing some support on another thread I received some information regarding a more optimized approach to shifting the contents of an array. Another community member, @steego , brought several variations of this sort of data manipulation to my attention.

With my curiosity piqued, I set about writing a few classes to test his suggestions and get a clear illustration of which approach is actually the most efficient, but I digress. Who cares how this all came about? You just want to optimize your code and get back to business, right? Let’s continue!

Question

What is the most efficient way to generate and shift a collection of integers? What is the best way to store these integers? Moreover, what is the best way to test the efficiency of a block of code and compare it against other solutions that produce the same result?

Methodology

I used two custom classes to test this. I set these classes up in such a way that you could simply place a method before and after your block of code and those methods work in tandem to return the time elapsed between them to the debug console. Simple enough, right?

The first class I’m going to outline is the static class that I used to call these methods. It just contains a Dictionary and two methods.

using System.Collections.Generic;
using hamsterbyte.MethodTimer;

public static class MethodTimer {

    public static Dictionary<string, MethodTimerObject> methodDictionary;

    public static void Start(string name) {
        if(methodDictionary == null)
                methodDictionary = new Dictionary<string, MethodTimerObject>();

        methodDictionary.Add(name, new MethodTimerObject(name));
        methodDictionary[name].Start();
    }

    public static void End(string name) {
        if(methodDictionary != null) {
            if(methodDictionary.ContainsKey(name)){
                methodDictionary[name].End();
                methodDictionary.Remove(name);
            }
        }
    }

}

The second class is the MethodTimerObject itself. This class serves as a container for individual timers and their data/methods.

using System.Diagnostics;
namespace hamsterbyte.MethodTimer
{
    public class MethodTimerObject
    {
        private Stopwatch _stopWatch;
        private string _name;
        private float _startTime;

        public string Name { get { return _name; } set { _name = value; } }

        public MethodTimerObject (string name)
        {
            _name = name;
        }

        public void Start ()
        {
            _stopWatch = new Stopwatch();
            _stopWatch.Start();
        }

        public void End ()
        {
            UnityEngine.Debug.Log ("\"" + _name + "\"" + " completed in " + _stopWatch.ElapsedMilliseconds + "ms");
            _stopWatch.Stop();
        }
    }
}

Lastly, I used a basic Monobehaviour to test and interact with these classes and the data in question. I used a few different data containers: LinkedList, Queue, and of course an int[ ] array.

The first thing I did was write methods to populate each of these containers with 5 million random integers. Then I shifted the data in each of these containers once to the left using several different methods.

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

public class Example : MonoBehaviour {

    Queue<int> _theQueue = new Queue<int>();
    LinkedList<int> _theList = new LinkedList<int>();
    int[] _theArray = new int[5000000];

    // Use this for initialization
    void Start () {

        MethodTimer.Start("Populate List");
        PopulateList();
        MethodTimer.End("Populate List");

        MethodTimer.Start("Populate Queue");
        PopulateQueue();
        MethodTimer.End("Populate Queue");

        MethodTimer.Start("Populate Array");
        PopulateArray();
        MethodTimer.End("Populate Array");

        MethodTimer.Start("Shift Loop Array");
        _theArray = Shift(_theArray);
        MethodTimer.End("Shift Loop Array");

        MethodTimer.Start("Shift Copy Array");
        _theArray = ShiftCopy(_theArray);
        MethodTimer.End("Shift Copy Array");

        MethodTimer.Start("Shift List");
        ShiftList();
        MethodTimer.End("Shift List");

        MethodTimer.Start("Shift Queue");
        ShiftQueue();
        MethodTimer.End("Shift Queue");

    }

    public void PopulateArray() {
        System.Random r = new System.Random();
        for(int i = 0; i < _theArray.Length; i++){
            _theArray[i] = r.Next(0, 100);
        }
    }

    public void PopulateQueue(){
        System.Random r = new System.Random();
        for(int i = 0; i < 5000000; i ++)
            _theQueue.Enqueue(r.Next(0, 100));
    }

    public void PopulateList() {
        System.Random r = new System.Random();
        for(int i = 0; i < 5000000; i++)
            _theList.AddLast(r.Next(0, 100));
    }

    public int[] Shift(int[] myArray){
        int[] tArray = new int[myArray.Length];
        for(int i = 0; i < myArray.Length; i++){
            if(i < myArray.Length - 1)
                tArray[i] = myArray[i + 1];
            else
                tArray[i] = myArray[0];
        }
        return tArray;
    }

    public int[] ShiftCopy(int[] myArray)
    {
        int[] tArray = new int[myArray.Length];
        int v = myArray[0];
        Array.Copy(myArray, 1, tArray, 0, myArray.Length - 1);
        tArray[tArray.Length - 1] = v;
        return tArray;
    }



    public void ShiftQueue () {
        int v = _theQueue.Dequeue();
        _theQueue.Enqueue(v);
    }

    public void ShiftList () {
        int v = _theList.First.Value;
        _theList.RemoveFirst();
        _theList.AddLast(v);
    }

}

Using these two classes to return the time elapsed during the execution of a code block returned some very interesting results. Keep in mind that I am not claiming this is the best way to test something like this; apparently the built in profiler has similar methods to the ones I’ve written. You can enclose any code you want between the two methods as illustrated above and it will return a debug statement telling you how long it took to execute that code. Let’s move on.

Results

The results of my testing with this methodology were quite interesting and they lead me to believe this same type of procedure could be applied to any code; this methodology, though unrefined, is universal.

Here’s a graphic displaying a block of results in the debugger:

2107006--138104--Optimization findings.png

I took the liberty of sorting these in the script by order of their efficiency. As you can see the speed with which I was able to populate the data in various containers differed quite a bit; LinkedList took the longest to populate and Queue was a close second behind the array.

Shifting this data was a whole different story. I used two methods for shifting the data in the array: A basic for loop and a copy and replace style method (more details can be gleaned from the Monobehaviour above). I found that the for loop was on average 10x slower than the copy and replace method.

When shifting the LinkedList and Queue things got really interesting Both of these methods returned an average of less than 1ms; in fact, most of the time they returned 0ms.

Your results will vary every time you check the elapsed time of your code, this is not an error, it is the expected behaviour. The best thing to do is compile a large amount of results and calculate the average; you will have outliers in your data set.

Summary

In closing, I think all developers should be aware of good optimization techniques. This is especially important if you intend to develop applications for mobile platforms. Mobile devices have come a long way in the last few years, but they still pale in comparison to a console and especially to a high end gaming PC.

The best thing to do is always be aware of optimization. Make yourself knowledgeable about good optimization practices and apply that in your work. Feel free to use the scripts in this thread to test the efficiency of your code. Completely open-source. I may refine the concept even more and set up a public repository for the project on git. If anyone has any suggestions for refining my methodology I would love to hear them as I intend to use this to optimize blocks of my code from this point forward. Thanks for taking the time to read this thread and best of luck to each and every one of you in your future endeavours!

6 Likes

Very good explanation aswell as a nice image to illustrate the result. Thanks alot for your work.

1 Like

If this helps some people understand optimization concepts a little better then I feel like I’ve done something good. I’m sorry we kind of hijacked your thread with a discussion on optimization. On the bright side, I think we can all use this to help us out with our own optimization, so some good has definitely come from it. Feel free to use this as you see fit. It’s such a simple idea and I think it should be available to everyone.

Nice writeup, and somewhat expected results. For a Queue, enqueuing and dequeuing are O(1) operations, the same goes for a LinkedList, where Remove/Add First/Last are all O(1) operations.

Shifting by looping, by requiring a loop, is however an O(n) operation.

Array.Copy is the odd one out, I’m guessing it relies on some form of mem-copy internally, so is also O(n), but it operates on bigger chunks.

The most surprising result to me is the time it takes to populate the list, I can’t see a reason for this taking so much longer.

It might be an idea to try generating the list and queue from the array.

Queue<int> q = new Queue<int>(array);
LinkedList<int> l = new LinkedList<int>(array);

I just came across this, http://bigocheatsheet.com you might find this interesting.

1 Like

Thanks for the writeup, those results look interesting. Worst of all, didn’t even know there is a built in Queue class =.=’

Thanks, I’ve bookmarked that page as I think it will be quite useful in the future. I went ahead and populated the list and queue from the values in the array. There is no appreciable difference.

2107034--138110--Optimization findings.png

Now, you do, and hopefully you have some idea about code optimization now :slight_smile:

I’ve decided to check performance for a thing that I wondered for some time; does using properties impact performance since they use method calls under the hood?

I’ve written a mockup script, something like:

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

public class CodePerformanceTester : MonoBehaviour
{
    Stopwatch stopwatch1 = new Stopwatch();
    Stopwatch stopwatch2 = new Stopwatch();
    int testVariable = 0;
    public int TestProperty
    {
        get
        {
            return testVariable;
        }
        set
        {
            testVariable = value;
        }
    }

    // Use this for initialization
    void Start ()
    {
        Invoke("Test", 1f);
    }
   
    void Test()
    {
        stopwatch1.Start();

        for (int i = 0; i < 1000000; i++)
        {
            testVariable = i;
        }

        stopwatch1.Stop();

        long result1 = stopwatch1.ElapsedMilliseconds;

        stopwatch2.Start();
       
        for (int i = 0; i < 1000000; i++)
        {
            TestProperty = i;
        }
       
        stopwatch2.Stop();

        long result2 = stopwatch2.ElapsedMilliseconds;

        UnityEngine.Debug.Log("Set variable time: "+result1+"ms");
        UnityEngine.Debug.Log("Set property time: "+result2+"ms");
    }
}

And results were somewhat expected:

Property time varied between: 13-19ms when I tested this multiple times. Hope this helps someone further optimise their code! :slight_smile:

3 Likes

@Fajlworks Good find! I always suspected this to be the case. A few milliseconds may seem negligible, but if you are not careful that can certainly add up.

Your example code does shift list twice, where it seemingly should be once shiftlist and once shiftqueue.

        MethodTimer.Start("Shift List");
        ShiftList();
        MethodTimer.End("Shift List");
        MethodTimer.Start("Shift Queue");
        ShiftList();
        MethodTimer.End("Shift Queue");
1 Like

#facepalm Complete and total oversight. This has been addressed and the code has been updated to reflect the change. However, it made no difference in the completion time of the method. Which means I don’t need to change the image :slight_smile:

Nice write up. Since we lost the bench marking article over on Unity gems it’s worth having something like this available.

It’s worth noting the profiler can do this, using the BeginSample and EndSample methods.

I’ll also include my regular warning about preoptimisation. It’s easy to get so caught up in tiny optimisations that the big picture code gets unreadable and unmaintainable. Finish the project first. Then do big picture optimisations. Then drop down to individual lines of hot code.

3 Likes

Yeah, they don’t say “Premature optimization is the root of all evil” for nothing :slight_smile:

2 Likes

So the profiler does have this capability? This is news to me. Definitely going to look into that further! Thanks

Also, I completely agree with not getting caught up in optimization until later into a project. Don’t want to get too nit picky until you are at that stage in development. I always throw stuff down quick and dirty first.

You should see what happens if you use the constructor for Queue that takes a capacity, so that all the needed space is allocated up-front, instead of the default constructor.

2 Likes

Just ran another test and found that there was no appreciable difference in speed either way the Queue is constructed:

2107034--138110--Optimization findings.png2107748--138177--Optimization findings2.png

Was a good thought though. I thought it might improve it a bit as well, but according to my methodology one is as good as the other

EDIT

There was an error in my script. It is actually faster. Keep reading for more details.

Curious. Could you post your updated test code?

For reference, here is the Queue source code.

Ahhhhhhh, that’s better. I had a little issue in my script. It’s running much faster than a queue that is not preallocated.

There was no appreciable difference because I was setting them both to a new instance of Queue instead of using the existing allocation from the second Queue:

2107764--138182--Optimization findings.png

Here’s the source:

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

public class Example : MonoBehaviour
{


    LinkedList<int> _theList = new LinkedList<int> ();
    int[] _theArray = new int[5000000];
    int[] _tempArray = new int[5000000];
    Queue<int> _preAllocatedQueue = new Queue<int>(5000000);
    Queue<int> _unallocatedQueue = new Queue<int>();

    // Use this for initialization
    void Start ()
    {
        MethodTimer.Start ("Populate Array Sorted");
        PopulateArraySorted ();
        MethodTimer.End ("Populate Array Sorted");

        MethodTimer.Start ("Populate Queue");
        PopulateQueue ();
        MethodTimer.End ("Populate Queue");

        MethodTimer.Start ("Populate Preallocated Queue");
        PopulatePreQueue ();
        MethodTimer.End ("Populate Preallocated Queue");

    }

    public void PopulateArrayRandom ()
    {
        System.Random r = new System.Random ();
        for (int i = 0; i < _theArray.Length; i++) {
            _theArray [i] = r.Next (0, 100);
        }
    }

    public void PopulateArraySorted ()
    {
        System.Random r = new System.Random ();
        for (int i = 0; i < _theArray.Length; i++) {
            _theArray [i] = r.Next (0, 100);
        }
    }

    public void PopulateQueue ()
    {
        _unallocatedQueue = new Queue<int> (_theArray);
    }

    public void PopulatePreQueue ()
    {
        for(int i = 0; i < _theArray.Length; i++){
            _preAllocatedQueue.Enqueue(_theArray[i]);
        }
    }

    public void PopulateList ()
    {
        _theList = new LinkedList<int> (_theArray);
    }

    public int[] Shift (int[] myArray)
    {
        int[] tArray = new int[myArray.Length];
        for (int i = 0; i < myArray.Length; i++) {
            if (i < myArray.Length - 1)
                tArray [i] = myArray [i + 1];
            else
                tArray [i] = myArray [0];
        }
        return tArray;
    }

    public int[] ShiftCopy (int[] myArray)
    {
        int[] tArray = new int[myArray.Length];
        int v = myArray [0];
        Array.Copy (myArray, 1, tArray, 0, myArray.Length - 1);
        tArray [tArray.Length - 1] = v;
        return tArray;
    }

    public int[] BubbleSort (int[] myArray)
    {
        int length = myArray.Length;
        int temp = myArray [0];
        for (int i = 0; i < length; i++) {
            for (int j = i+1; j < length; j++) {
                if (myArray [i] > myArray [j]) {
                    temp = myArray [i];
                    myArray [i] = myArray [j];
                    myArray [j] = temp;
                }
            }
        }
        return myArray;      
    }

    public int[] InsertionSort (int[] myArray)
    {
        for (int i = 0; i < myArray.Length-1; i++) {
            for (int j = i + 1; j > 0; j--) {
                if (myArray [j - 1] > myArray [j]) {
                    int temp = myArray [j - 1];
                    myArray [j - 1] = myArray [j];
                    myArray [j] = temp;
                }
            }
        }
        return myArray;
    }

    public int[] SelectionSort(int[] myArray) {
        int i;
        int N = myArray.Length;
      
        for (i=0; i < N-1; i++) {
            int k = MinInArray (myArray, i);
            if (i != k)
                Exchange (myArray, i, k);
        }
        return myArray;
    }

    public static void Exchange (int[] myArray, int m, int n)
    {
        int temporary;
        temporary = myArray [m];
        myArray [m] = myArray [n];
        myArray [n] = temporary;
    }

    public int MinInArray(int[] myArray, int start) {
        int minPos = start;
        for (int pos=start+1; pos < myArray.Length; pos++)
            if (myArray [pos] < myArray [minPos])
                minPos = pos;
        return minPos;
    }

    public int FindIndexByLoop(int[] myArray, int value) {
        for(int i = 0; i < myArray.Length; i++)
        {
            if(myArray[i] == value)
                return i;
        }
        return -1;
    }

    public int BinarySearch(int[] arr, int lowBound, int highBound, int value)
    {
        int mid;
        while (lowBound <= highBound)
        {
            mid = (lowBound + highBound) / 2;
            if (arr[mid]<value)
            {
                lowBound = mid + 1;
                continue;
            }
            else if (arr[mid] > value)
            {
                highBound = mid - 1;
                continue;
            }
            else
            {
                return mid;
            }
        }
        return -1;//value not found
    }



    public void ShiftQueue ()
    {
        //int v = _theQueue.Dequeue ();
        //_theQueue.Enqueue (v);
    }

    public void ShiftList ()
    {
        int v = _theList.First.Value;
        _theList.RemoveFirst ();
        _theList.AddLast (v);
    }

}

Good catch superpig!

1 Like

While pretty off topic, have any of the issue with “for each” loops been fixed; or should everyone still hold onto the habit of avoiding them like the Black Plague?

It’s really a pity, as they do increase readability quite considerably; and while we’re at it: they’re much more type safe.

I could be wrong but as far as I know for each iteration is still slower than a normal for loop. You can certainly test that theory from the classes I’ve posted above. I’d test it right now to verify but I’m away from my computer. If you do test it go ahead and post your results here. We’d like to see them For sure.