Search Unity

Code Optimization Practices

Discussion in 'Scripting' started by hamsterbytedev, May 10, 2015.

?

How familiar are you with code optimization?

  1. Very familiar; I always keep optimization in mind and try to use the best practices.

    20 vote(s)
    48.8%
  2. Somewhat familiar; I know it is a big deal and I do what I can.

    12 vote(s)
    29.3%
  3. I've heard of it; I know what it is, I know it is a big deal, but I've not done much.

    5 vote(s)
    12.2%
  4. What's optimization? Bro, I have Quad SLI Titan X's, 64GB of DDR4, and a 5970K. Who cares!

    4 vote(s)
    9.8%
  1. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    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.

    Code (CSharp):
    1.  
    2. using System.Collections.Generic;
    3. using hamsterbyte.MethodTimer;
    4.  
    5. public static class MethodTimer {
    6.  
    7.     public static Dictionary<string, MethodTimerObject> methodDictionary;
    8.  
    9.     public static void Start(string name) {
    10.         if(methodDictionary == null)
    11.                 methodDictionary = new Dictionary<string, MethodTimerObject>();
    12.  
    13.         methodDictionary.Add(name, new MethodTimerObject(name));
    14.         methodDictionary[name].Start();
    15.     }
    16.  
    17.     public static void End(string name) {
    18.         if(methodDictionary != null) {
    19.             if(methodDictionary.ContainsKey(name)){
    20.                 methodDictionary[name].End();
    21.                 methodDictionary.Remove(name);
    22.             }
    23.         }
    24.     }
    25.  
    26. }
    27.  
    The second class is the MethodTimerObject itself. This class serves as a container for individual timers and their data/methods.
    Code (CSharp):
    1.  
    2. using System.Diagnostics;
    3. namespace hamsterbyte.MethodTimer
    4. {
    5.     public class MethodTimerObject
    6.     {
    7.         private Stopwatch _stopWatch;
    8.         private string _name;
    9.         private float _startTime;
    10.  
    11.         public string Name { get { return _name; } set { _name = value; } }
    12.  
    13.         public MethodTimerObject (string name)
    14.         {
    15.             _name = name;
    16.         }
    17.  
    18.         public void Start ()
    19.         {
    20.             _stopWatch = new Stopwatch();
    21.             _stopWatch.Start();
    22.         }
    23.  
    24.         public void End ()
    25.         {
    26.             UnityEngine.Debug.Log ("\"" + _name + "\"" + " completed in " + _stopWatch.ElapsedMilliseconds + "ms");
    27.             _stopWatch.Stop();
    28.         }
    29.     }
    30. }
    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<int>, Queue<int>, 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.

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System;
    4. using System.Collections.Generic;
    5.  
    6. public class Example : MonoBehaviour {
    7.  
    8.     Queue<int> _theQueue = new Queue<int>();
    9.     LinkedList<int> _theList = new LinkedList<int>();
    10.     int[] _theArray = new int[5000000];
    11.  
    12.     // Use this for initialization
    13.     void Start () {
    14.  
    15.         MethodTimer.Start("Populate List");
    16.         PopulateList();
    17.         MethodTimer.End("Populate List");
    18.  
    19.         MethodTimer.Start("Populate Queue");
    20.         PopulateQueue();
    21.         MethodTimer.End("Populate Queue");
    22.  
    23.         MethodTimer.Start("Populate Array");
    24.         PopulateArray();
    25.         MethodTimer.End("Populate Array");
    26.  
    27.         MethodTimer.Start("Shift Loop Array");
    28.         _theArray = Shift(_theArray);
    29.         MethodTimer.End("Shift Loop Array");
    30.  
    31.         MethodTimer.Start("Shift Copy Array");
    32.         _theArray = ShiftCopy(_theArray);
    33.         MethodTimer.End("Shift Copy Array");
    34.  
    35.         MethodTimer.Start("Shift List");
    36.         ShiftList();
    37.         MethodTimer.End("Shift List");
    38.  
    39.         MethodTimer.Start("Shift Queue");
    40.         ShiftQueue();
    41.         MethodTimer.End("Shift Queue");
    42.  
    43.     }
    44.  
    45.     public void PopulateArray() {
    46.         System.Random r = new System.Random();
    47.         for(int i = 0; i < _theArray.Length; i++){
    48.             _theArray[i] = r.Next(0, 100);
    49.         }
    50.     }
    51.  
    52.     public void PopulateQueue(){
    53.         System.Random r = new System.Random();
    54.         for(int i = 0; i < 5000000; i ++)
    55.             _theQueue.Enqueue(r.Next(0, 100));
    56.     }
    57.  
    58.     public void PopulateList() {
    59.         System.Random r = new System.Random();
    60.         for(int i = 0; i < 5000000; i++)
    61.             _theList.AddLast(r.Next(0, 100));
    62.     }
    63.  
    64.     public int[] Shift(int[] myArray){
    65.         int[] tArray = new int[myArray.Length];
    66.         for(int i = 0; i < myArray.Length; i++){
    67.             if(i < myArray.Length - 1)
    68.                 tArray[i] = myArray[i + 1];
    69.             else
    70.                 tArray[i] = myArray[0];
    71.         }
    72.         return tArray;
    73.     }
    74.  
    75.     public int[] ShiftCopy(int[] myArray)
    76.     {
    77.         int[] tArray = new int[myArray.Length];
    78.         int v = myArray[0];
    79.         Array.Copy(myArray, 1, tArray, 0, myArray.Length - 1);
    80.         tArray[tArray.Length - 1] = v;
    81.         return tArray;
    82.     }
    83.  
    84.  
    85.  
    86.     public void ShiftQueue () {
    87.         int v = _theQueue.Dequeue();
    88.         _theQueue.Enqueue(v);
    89.     }
    90.  
    91.     public void ShiftList () {
    92.         int v = _theList.First.Value;
    93.         _theList.RemoveFirst();
    94.         _theList.AddLast(v);
    95.     }
    96.  
    97. }
    98.  
    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:

    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<int> took the longest to populate and Queue<int> 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<int> and Queue<int> 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!
     
    Last edited: May 10, 2015
    leni8ec, ssojyeti2, rakkarage and 3 others like this.
  2. MrDahl

    MrDahl

    Joined:
    Oct 29, 2013
    Posts:
    5
    Very good explanation aswell as a nice image to illustrate the result. Thanks alot for your work.
     
    hamsterbytedev likes this.
  3. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    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.
     
  4. steego

    steego

    Joined:
    Jul 15, 2010
    Posts:
    969
    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.

    Code (csharp):
    1.  
    2. Queue<int> q = new Queue<int>(array);
    3. LinkedList<int> l = new LinkedList<int>(array);
    4.  
    I just came across this, http://bigocheatsheet.com you might find this interesting.
     
    hamsterbytedev likes this.
  5. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Thanks for the writeup, those results look interesting. Worst of all, didn't even know there is a built in Queue class =.='
     
  6. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    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.

    Optimization findings.png
     
  7. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    Now, you do, and hopefully you have some idea about code optimization now :)
     
  8. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    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:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Diagnostics;
    4.  
    5. public class CodePerformanceTester : MonoBehaviour
    6. {
    7.     Stopwatch stopwatch1 = new Stopwatch();
    8.     Stopwatch stopwatch2 = new Stopwatch();
    9.     int testVariable = 0;
    10.     public int TestProperty
    11.     {
    12.         get
    13.         {
    14.             return testVariable;
    15.         }
    16.         set
    17.         {
    18.             testVariable = value;
    19.         }
    20.     }
    21.  
    22.     // Use this for initialization
    23.     void Start ()
    24.     {
    25.         Invoke("Test", 1f);
    26.     }
    27.    
    28.     void Test()
    29.     {
    30.         stopwatch1.Start();
    31.  
    32.         for (int i = 0; i < 1000000; i++)
    33.         {
    34.             testVariable = i;
    35.         }
    36.  
    37.         stopwatch1.Stop();
    38.  
    39.         long result1 = stopwatch1.ElapsedMilliseconds;
    40.  
    41.         stopwatch2.Start();
    42.        
    43.         for (int i = 0; i < 1000000; i++)
    44.         {
    45.             TestProperty = i;
    46.         }
    47.        
    48.         stopwatch2.Stop();
    49.  
    50.         long result2 = stopwatch2.ElapsedMilliseconds;
    51.  
    52.         UnityEngine.Debug.Log("Set variable time: "+result1+"ms");
    53.         UnityEngine.Debug.Log("Set property time: "+result2+"ms");
    54.     }
    55. }
    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! :)
     
    leni8ec, landon912 and hamsterbytedev like this.
  9. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    @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.
     
  10. Zuntatos

    Zuntatos

    Joined:
    Nov 18, 2012
    Posts:
    612
    Your example code does shift list twice, where it seemingly should be once shiftlist and once shiftqueue.

    Code (CSharp):
    1.  
    2.         MethodTimer.Start("Shift List");
    3.         ShiftList();
    4.         MethodTimer.End("Shift List");
    5.         MethodTimer.Start("Shift Queue");
    6.         ShiftList();
    7.         MethodTimer.End("Shift Queue");
     
    hamsterbytedev likes this.
  11. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    #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 :)
     
  12. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    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.
     
  13. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Yeah, they don't say "Premature optimization is the root of all evil" for nothing :)
     
    passerbycmc and Kiwasi like this.
  14. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    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.
     
  15. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,660
    ssojyeti2 and Kiwasi like this.
  16. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    Just ran another test and found that there was no appreciable difference in speed either way the Queue is constructed:

    Optimization findings.png 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.
     
    Last edited: May 11, 2015
  17. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,660
  18. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    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:

    Optimization findings.png

    Here's the source:

    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3. using System.Collections.Generic;
    4.  
    5. public class Example : MonoBehaviour
    6. {
    7.  
    8.  
    9.     LinkedList<int> _theList = new LinkedList<int> ();
    10.     int[] _theArray = new int[5000000];
    11.     int[] _tempArray = new int[5000000];
    12.     Queue<int> _preAllocatedQueue = new Queue<int>(5000000);
    13.     Queue<int> _unallocatedQueue = new Queue<int>();
    14.  
    15.     // Use this for initialization
    16.     void Start ()
    17.     {
    18.         MethodTimer.Start ("Populate Array Sorted");
    19.         PopulateArraySorted ();
    20.         MethodTimer.End ("Populate Array Sorted");
    21.  
    22.         MethodTimer.Start ("Populate Queue");
    23.         PopulateQueue ();
    24.         MethodTimer.End ("Populate Queue");
    25.  
    26.         MethodTimer.Start ("Populate Preallocated Queue");
    27.         PopulatePreQueue ();
    28.         MethodTimer.End ("Populate Preallocated Queue");
    29.  
    30.     }
    31.  
    32.     public void PopulateArrayRandom ()
    33.     {
    34.         System.Random r = new System.Random ();
    35.         for (int i = 0; i < _theArray.Length; i++) {
    36.             _theArray [i] = r.Next (0, 100);
    37.         }
    38.     }
    39.  
    40.     public void PopulateArraySorted ()
    41.     {
    42.         System.Random r = new System.Random ();
    43.         for (int i = 0; i < _theArray.Length; i++) {
    44.             _theArray [i] = r.Next (0, 100);
    45.         }
    46.     }
    47.  
    48.     public void PopulateQueue ()
    49.     {
    50.         _unallocatedQueue = new Queue<int> (_theArray);
    51.     }
    52.  
    53.     public void PopulatePreQueue ()
    54.     {
    55.         for(int i = 0; i < _theArray.Length; i++){
    56.             _preAllocatedQueue.Enqueue(_theArray[i]);
    57.         }
    58.     }
    59.  
    60.     public void PopulateList ()
    61.     {
    62.         _theList = new LinkedList<int> (_theArray);
    63.     }
    64.  
    65.     public int[] Shift (int[] myArray)
    66.     {
    67.         int[] tArray = new int[myArray.Length];
    68.         for (int i = 0; i < myArray.Length; i++) {
    69.             if (i < myArray.Length - 1)
    70.                 tArray [i] = myArray [i + 1];
    71.             else
    72.                 tArray [i] = myArray [0];
    73.         }
    74.         return tArray;
    75.     }
    76.  
    77.     public int[] ShiftCopy (int[] myArray)
    78.     {
    79.         int[] tArray = new int[myArray.Length];
    80.         int v = myArray [0];
    81.         Array.Copy (myArray, 1, tArray, 0, myArray.Length - 1);
    82.         tArray [tArray.Length - 1] = v;
    83.         return tArray;
    84.     }
    85.  
    86.     public int[] BubbleSort (int[] myArray)
    87.     {
    88.         int length = myArray.Length;
    89.         int temp = myArray [0];
    90.         for (int i = 0; i < length; i++) {
    91.             for (int j = i+1; j < length; j++) {
    92.                 if (myArray [i] > myArray [j]) {
    93.                     temp = myArray [i];
    94.                     myArray [i] = myArray [j];
    95.                     myArray [j] = temp;
    96.                 }
    97.             }
    98.         }
    99.         return myArray;      
    100.     }
    101.  
    102.     public int[] InsertionSort (int[] myArray)
    103.     {
    104.         for (int i = 0; i < myArray.Length-1; i++) {
    105.             for (int j = i + 1; j > 0; j--) {
    106.                 if (myArray [j - 1] > myArray [j]) {
    107.                     int temp = myArray [j - 1];
    108.                     myArray [j - 1] = myArray [j];
    109.                     myArray [j] = temp;
    110.                 }
    111.             }
    112.         }
    113.         return myArray;
    114.     }
    115.  
    116.     public int[] SelectionSort(int[] myArray) {
    117.         int i;
    118.         int N = myArray.Length;
    119.      
    120.         for (i=0; i < N-1; i++) {
    121.             int k = MinInArray (myArray, i);
    122.             if (i != k)
    123.                 Exchange (myArray, i, k);
    124.         }
    125.         return myArray;
    126.     }
    127.  
    128.     public static void Exchange (int[] myArray, int m, int n)
    129.     {
    130.         int temporary;
    131.         temporary = myArray [m];
    132.         myArray [m] = myArray [n];
    133.         myArray [n] = temporary;
    134.     }
    135.  
    136.     public int MinInArray(int[] myArray, int start) {
    137.         int minPos = start;
    138.         for (int pos=start+1; pos < myArray.Length; pos++)
    139.             if (myArray [pos] < myArray [minPos])
    140.                 minPos = pos;
    141.         return minPos;
    142.     }
    143.  
    144.     public int FindIndexByLoop(int[] myArray, int value) {
    145.         for(int i = 0; i < myArray.Length; i++)
    146.         {
    147.             if(myArray[i] == value)
    148.                 return i;
    149.         }
    150.         return -1;
    151.     }
    152.  
    153.     public int BinarySearch(int[] arr, int lowBound, int highBound, int value)
    154.     {
    155.         int mid;
    156.         while (lowBound <= highBound)
    157.         {
    158.             mid = (lowBound + highBound) / 2;
    159.             if (arr[mid]<value)
    160.             {
    161.                 lowBound = mid + 1;
    162.                 continue;
    163.             }
    164.             else if (arr[mid] > value)
    165.             {
    166.                 highBound = mid - 1;
    167.                 continue;
    168.             }
    169.             else
    170.             {
    171.                 return mid;
    172.             }
    173.         }
    174.         return -1;//value not found
    175.     }
    176.  
    177.  
    178.  
    179.     public void ShiftQueue ()
    180.     {
    181.         //int v = _theQueue.Dequeue ();
    182.         //_theQueue.Enqueue (v);
    183.     }
    184.  
    185.     public void ShiftList ()
    186.     {
    187.         int v = _theList.First.Value;
    188.         _theList.RemoveFirst ();
    189.         _theList.AddLast (v);
    190.     }
    191.  
    192. }
    193.  
    Good catch superpig!
     
    superpig likes this.
  19. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    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.
     
  20. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    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.
     
  21. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    If its still allocating an enumerator, then yes, they should still be avoided.

    safer bet is to just avoid them. Not sure why you think they are considered more typesafe though?
     
    hamsterbytedev likes this.
  22. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    For arrays and lists I tend to use for loops. They end up being more flexible, and also let you do naughty things like modify a collection as you iterate through it.

    foreach does make sense on collections that a for loop just won't for on. Like a dictionary or hashset. It also lets you run code on any enumerator. And this lets you do fun things like coroutines.

    So right loop for the right job.
     
    hamsterbytedev likes this.
  23. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    Agreed. If foreach loops didn't have a purpose they would probably just be deprecated and wouldn't exist anymore. They just aren't as efficient as a traditional for loop. I prefer not to use them at all unless I have to iterate over a dataset that can't be accessed by way of an index; like the two examples you gave.
     
  24. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I still use them a lot. Especially during early development. foreach is easier to read, and more forgiving on changing the underlying collection type. I use a lot of custom collections, its sometimes easier to use a generic foreach then to redo the for loops each time the underlying collection changes.
     
  25. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    I haven't done much with custom collections. I haven't had to with all the existing ways to store a collection of data. If I need to store a custom data class, like a class that contained information about an item for example, I tend to favour arrays, lists, or dictionaries to aggregate a bunch of them and iterate through them that way. Would you say this is bad practice, if so, what kind of benefit does using a custom collection offer? I'm sure I'm not the only person who is curious about this :)
     
  26. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Custom collections are when I want my data just so. There are plenty of things you can do with the regular collections, but also plenty of things you can't do.

    For example in my infinite terrain generator I've implemented a Map collection. This collection is generic, can be indexed like a 2D array, expands infinitely automatically, does not throw index out of range exceptions, allows for negative indexes, and is O(1) for access operations. You would struggle to find one of the built in collections that does exactly this.

    Most of the time custom collections are simply wrappers for the built in collections. Just like a List is a wrapper for an array. My Map collection wraps a set of four 2D arrays. But I hardly want to keep the logic holding the four arrays together to be interspersed through my code base. With a custom collection I can simply access Map[4,-5] and forget about what is happening underneath.
     
    landon912 and hamsterbytedev like this.
  27. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    Yep, that's a very valid point. I've never had to deal with anything like that, so the topic never really crossed my mind. It's quite an interesting approach though. I'll make a note of it in case I have to use a similar approach to something in the future. Cheers!
     
  28. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,660
    There was never an issue with foreach loops when used with arrays.

    When used with other standard collections, they allocate a small amount of memory (usually about 24 bytes) once the loop has finished running. I don't think it's the terrifying result that people make it out to be - especially in code that doesn't run every single frame, and especially not on desktop platforms - and that it's only appropriate to worry about it once profiling shows it to be a problem; it's also easily solved by just moving your code to a DLL that you compile in Visual Studio, instead of loose scripts in Unity, so it's the sort of thing you can just do at the end of a project to tidy things up.
     
  29. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,660
    Just to add a little more detail:

    When you use a foreach loop with an array, internally it's compiled to use a loop counter variable - i.e. what the CPU ends up doing should be pretty much identical to what it does for a for loop.

    When you use a foreach loop with a standard collection - System.Collections.Generic.List<T> and so on - then an enumerator is created (as a struct, so no GC allocation - the 24 bytes I mentioned comes from an inefficiency in the way we clean up at the end of the loop). Each iteration of the loop, MoveNext() is called. Here is the implementation of MoveNext for a List; you can see it does a bit more work than a basic for-loop would, checking that the list has not been modified while you are iterating over it, that the iterator is still within the bounds of the list, etc. This is unlikely to have a significant performance impact compared to using a for-loop, though... and if it turns out that it does, you can always change to an index-based loop later on easily. That sort of thing is a micro-optimisation, though, and is exactly the kind of peephole optimisation that a compiler should be taking care of for you. (Once we have an up-to-date compiler, anyway...)

    Also, just a thought regarding analysis of some of this stuff: things like Big-O notation are useful, but you should never lose sight of the actual raw time taken by things. It might be that adding items to data structure A is O(1) while adding items to data structure B is O(n); so it sounds like A is a better choice, right? But O(1) just means 'constant time' - it doesn't say anything about what that time is. It could be that adding an item to data structure A always takes 1ms. Meanwhile, adding an item to data structure B might take n*0.01ms - so while it could become more expensive with a lot of items, if you know that in practice it's going to have fewer than 100, it's a better choice.
     
    landon912, hippocoder, Kiwasi and 4 others like this.
  30. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    @superpig is a fountain of useful information. I too noticed discrepancies between the Big-O information and real world application; specifically in sorting an array. Moreover, the for vs foreach argument will rage on until the efficiencies of both loops are equal. It isn't really relevant to the discriminating optimizer that the foreach loop isn't actually that much slower.

    I ran tests against each of these to get an accurate idea of just what the difference was. I populated an array from an array using both loop styles and an array from a list using both loops styles. On a dataset of 5 million integers there was a ~10ms difference in the array to array population and a ~150-200 millisecond difference in the list to array population; always in favour of the standard for loop. This may seem negligible, but it's still a reality.

    I guess the point I'm trying to make is that it may seem picky to choose a for loop over a foreach loop the fact remains that the for loop is slightly faster. Here's a good way to look at it - if someone actually wanted to put 4 Titan X's in their system, even though we all know SLI scaling depreciates more and more with every card you bridge, some would do it just because it would give them a minor increase in performance. The increase would be barely noticeable and it would only come up in certain situations. The kind of person who is willing to spend $1200 on another Titan to get an extra ~10 FPS is the kind of person who is going to flame you for using a foreach loop instead of a for loop.

    When it comes to shaving milliseconds off execution times the for loop always prevails because it does not require the extra instructions a foreach loop does. The difference might be small, and well within a margin that is more than acceptable, but for loops are still faster.

    I would like to see this eliminated by the compiler so we can finally settle the debate, but even when it is you are going to have proponents on both sides of the argument. I found myself in a similar debate last week on another thread about how to properly organize your code and make it more readable; a lot of this is personal preference.

    At the end of the day, if a few milliseconds bother you - it's seriously ~10ms over a 5 million point array of integers - then just use a for loop whenever you can and you don't have to worry about it. If you don't need that level of efficiency then it really doesn't matter which one you use. If you find that foreach is easier for you to read then just use it and don't concern yourself with the consequences - they are so slight you probably won't notice them anyway.

    All that being said, I do a fair bit of development for mobile platforms. Optimization here is incredibly important and I tend to be very anal about it. I have a few edge-case applications where I do a lot of iteration over lists and arrays and I was able to increase performance by eliminating foreach loops.and swapping lists for arrays where possible. Keep in mind that the applications to which I am referring are special case scenarios where I'm iterating over data at the end of every frame in a coroutine. You results may vary.
     
    Kiwasi likes this.
  31. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    I just use for straight up these days. First project used foreach, was a massive task going and finding everyone and replacing it with for loops. Foreach looks nicer, but only saves a marginal amount of typing
     
  32. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    That's basically my feeling in the subject too.
     
    JamesLeeNZ likes this.
  33. Deleted User

    Deleted User

    Guest

    Did you post this because of the way I use gameobjects as "folders" :p
    Great thread anyway.
    What about Memory Optimization? You should cover that too. :)
     
    Last edited by a moderator: May 12, 2015
  34. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    This is about memory optimization... not sure how you drew a correleation about folder structure to the op 's tests
     
  35. Deleted User

    Deleted User

    Guest

    Seems like he is testing the execution time of different methods & stuff and iterating over lists vs arrays. That's not memory optimization. Memory optimization is using a bigger memory footprint than you need. Not sure why you're mentioning folder structure because I never brought it up. I think you misunderstood my post.
     
  36. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    which are all in memory...

    I dont feel that the type of memory opt youre talking about has anything to do with code.
     
  37. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    This was not related in any way to the way you organize your game objects. Though, making extraneous objects should definitely be avoided for optimization, this thread is actually the result of a different unrelated conversation about the efficiency of data collections. Since you brought it up though, I can definitely go into more detail about why adding extraneous game objects is a bad idea and post some data from that as well. If anyone is interested?
     
  38. Deleted User

    Deleted User

    Guest

    Actually it does. Different objects have different memory footprints. A dictionary has a bigger memory footprint than an array. Other examples of memory optimization: loading an xml in memory when you don't need to; sprite batching.

    @Hamsterbyte.LLC Was just j/king about that. Yeah go ahead. :) Although now I trimmed it down to 1 object for scripts + 1 for graphics + 1 for collider
     
    Last edited by a moderator: May 12, 2015
  39. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    which is a so trivial its not even worth discussion... if you're picking array over dictionary because of the memory footprint, you're doing it wrong.

    sprite batching has nothing to do with code.
     
    hamsterbytedev and Kiwasi like this.
  40. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I dunno, could be relevant if you are coding in C++ for a micro processor. ;)
     
  41. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    absolutely!

    It's about picking the right thing for the right job. I wouldn't pick an array if I wanted direct item access, just like I wouldnt pick a dictionary if I was primarily going to iterate over a collection constantly.
     
  42. Deleted User

    Deleted User

    Guest

    That was just an example. You completely missed the point.

    I never said sprite batching had anything to do with code. Reread my post and tell me where I said it did. He posted about code optimization, and I said he should make a thread about memory optimization.
     
    Last edited by a moderator: May 12, 2015
  43. steego

    steego

    Joined:
    Jul 15, 2010
    Posts:
    969
    I agree. The good thing about knowing some Big-O analysis, is that you can more easily see which of your algorithms are good candidates for optimisation, and which alternatives might be a better substitute.

    That being said, I don't think this mantra has been brought up in this thread yet: When optimising, always profile. Then profile some more.

    Compilers and processors do so many crazy things that it's near impossible to reason about it. In some cases an obviously slower algorithm could outperform an obviously faster one because it just happens to fit in the processors cache for example.

    Even processors are sort-of compilers these days, compiling machine code into micro-ops and doing things like branch prediction and out of order execution.

    So don't trust your intuition, profile.
     
    Ryiah and Kiwasi like this.
  44. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    @steego your brilliance and willingness to help others is a great asset not only to this thread but to the community as a whole. Your advice is always good advice and it is clear to me that you have both the passion and expansive knowledge that it takes to excel in the software world. I believe I speak for all of us when I say never stop being that person. Cheers!

    On a less related note I will be testing memory footprints of various data collections as I find the time to do it. I'm looking at you @supremegrandruler .I will update the original thread when I have results to show. Be advised that @JamesLeeNZ has provided some useful, albeit scathing, insight on the matter.

    I'd like to keep this thread civil without starting any flame wars. The information contained herein is intended to help everyone, so let's try to be as kind and helpful as possible. The only stupid questions are the ones you don't ask. Instead of saying that something is trivial and not worth discussion, explain why you think it is trivial in a clear and concise manner instead of being dismissive or demeaning. Anyways, thanks for keeping the thread alive guys! This is a very productive conversation for all of us! :)
     
  45. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    If you're using:
    Code (CSharp):
    1. Vector3 someVector;
    2.  
    3. void Update()
    4. {
    5.      bool result = someVector.Equals( Vector3.one );
    6. }
    You might want to change that since it is always allocating 28bytes.


    Just create a utility class like:
    Code (CSharp):
    1. public class VectorUtilities
    2. {
    3.      public static bool Compare(Vector2 vec1, Vector2 vec2)
    4.     {
    5.         if (vec1.x == vec2.x &&
    6.             vec1.y == vec2.y)
    7.             return true;
    8.        
    9.         return false;
    10.     }
    11.  
    12.     public static bool Compare(Vector3 vec1, Vector3 vec2)
    13.     {
    14.         if (vec1.x == vec2.x &&
    15.             vec1.y == vec2.y &&
    16.             vec1.z == vec2.z)
    17.             return true;
    18.  
    19.         return false;
    20.     }
    21. }
    If you do manual check, you won't allocate them bytes which can be useful when used in Update() calls.
     
    hamsterbytedev likes this.
  46. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353

    I have my own methods for returning the memory allocation of a certain dataset; I won't be using the profiler for this either. I want people not only to understand that things are the way they are, but why they are that way, and how they can test the same thing anywhere without the unity profiler. My approach is one of full disclosure and though the profiler is quite awesome it doesn't tell you how it works; it just works and you come to rely on it. Everyone has a cell phone and nobody knows your phone number because they can get it at the touch of a button any time they want.

    Convenience is great, but it also stifles the mind and imagination. I think to be a great coder you have to know why things work, how they work, and not just that they do work.

    That being said, you've certainly made a noteworthy observation. One thing I would add is that GC.GetTotalMemory() returns bytes that are 'thought' to be allocated. Not sure what the implications of this are and I'm not sure if unity is using that to populate that particular field in the profiler, but I do know that thinking something is not the same as knowing something.
     
  47. Deleted User

    Deleted User

    Guest

    Not really, he didn't say anything I already knew: he just misunderstood everything from the beginning:
    - First he thought the "gameobjects folder" thing was addressed at him when it clearly wasn't, and ofc, ended up having no idea what he was talking about because he wasn't the one the reply was addressed to
    - Two he attacked an example instead of attacking the argument itself.
    - Three he misunderstood saying I said sprite batching had everything to do with coding when it was just an example of memory optimization I gave
    - Fourth, correct me if I'm using the terms incorrectly, but code execution speed has nothing to do with memory
     
    Last edited by a moderator: May 12, 2015
  48. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Code execution speed actually has a great deal to do with memory. The allocation and deallocation is one of the most costly things that slow down your code execution.
     
    hamsterbytedev and Deleted User like this.
  49. hamsterbytedev

    hamsterbytedev

    Joined:
    Dec 9, 2014
    Posts:
    353
    This is exactly correct. Memory allocation is expensive; this principal is clearly illustrated by the tests I ran between a preallocated queue and one that was not preallocated

     
  50. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    foreach will never get deprecated as it is very useful. The only reason it should be avoided in unity is because of the GC. In standard desktop apps, that 24 byte enumerator allocation is nothing, even run constantly. In Unity in an Update, its feeding the GC, which is what needs to be avoided.

    Ill just leave this here... because lol

     
    hamsterbytedev likes this.