Try/Catch vs If {} for performance in large arrays

Gday all,

When I was playing around with topographic map generation ( https://forum.unity3d.com/threads/wip-small-works-game-programming-thread.146445/page-27#post-2872694 ) I ended up using quite large 2D arrays (1000 x 1000 and greater).

As part of the vegetation generation, a basic heatmap of each tree is developed; each tree affects cells a few each side of it. The result of this required iteration a few indexes to the left/right of the array many times. In order to deal with array index out of bounds issues, I used a try catch instead of an if statement (not just out of laziness!)

Obviously the accepted way to do this is using an if statement each iteration to ensure that the index you are about to check is not outside the bounds.

My thoughts were that with 1000000 cells, the liklihood of a cell being outside the array index is quite low so the additional exception handling overhead wouldnt need to be called very often as opposed to the (still quite fast) if statement check every iteration.

All the research I have done indicates that the try catch used in this fashion is extremely poor form and wouldn’t net any notable benefits however based on my results, these statements may be referring to (much) smaller iteration sizes.

Tests
I ran a few tests using the profiler and came up with some interesting results.
Test setup

    public int size;
    public int[,] arrayTest;
    int temp;
   
    void Awake () {
        arrayTest = new int[size, size];
    }
   
    void Start()
    {
        StartCoroutine(DoTest());
    }

    IEnumerator DoTest()
    {
        yield return new WaitForSeconds(1);
        CheckTry();
        yield return new WaitForSeconds(2);
        CheckIf();
        yield return new WaitForSeconds(1);
        Debug.Break();
    }

(The temp variable is just to do some ‘work’ inside the loops as per below)

Test 1 : one index either side

    void CheckTry()
    {
        for (int x = 0; x < arrayTest.GetLength(1); x++)
        {
            for (int y = 0; y < arrayTest.GetLength(0); y++)
            {
                try
                {
                    temp = arrayTest[x - 1, y];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTest[x + 1, y];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTest[x, y-1];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTest[x, y+1];
                }
                catch (System.Exception e) { }

            }
        }
    }

    void CheckIf()
    {

        for (int x = 0; x < arrayTest.GetLength(1); x++)
        {
            for (int y = 0; y < arrayTest.GetLength(0); y++)
            {
                if (x > 0)
                {
                    temp = arrayTest[x - 1, y];
                }
                if (y > 0)
                {
                    temp = arrayTest[x, y - 1];
                }
                if (x < arrayTest.GetLength(1)-1)
                {
                    temp = arrayTest[x + 1, y];
                }
                if (y < arrayTest.GetLength(0)-1)
                {
                    temp = arrayTest[x, y + 1];
                }
            }
        }

Test 2 : Multiple indexes either side (closer to how I was developing the heat maps)

    void CheckTry()
    {
        for (int x = 0; x < arrayTest.GetLength(1); x++)
        {
            for (int y = 0; y < arrayTest.GetLength(0); y++)
            {
                for (int xOffset = -3; xOffset < 4; xOffset++)
                {
                    for (int yOffset = -3; yOffset < 4; yOffset++)
                    {

                        try
                        {
                            temp = arrayTest[x + xOffset, y + yOffset];
                        }
                        catch (System.Exception e) { }
                    }
                }
            }
        }
    }

    void CheckIf()
    {

        for (int x = 0; x < arrayTest.GetLength(1); x++)
        {
            for (int y = 0; y < arrayTest.GetLength(0); y++)
            {

                for (int xOffset = -3; xOffset < 4; xOffset++)
                {
                    for (int yOffset = -3; yOffset < 4; yOffset++)
                    {
                        if (x + xOffset >= 0 && x + xOffset < arrayTest.GetLength(1) && y + yOffset >= 0 && y + yOffset < arrayTest.GetLength(0))
                        {
                            temp = arrayTest[x + xOffset, y + yOffset];
                        }
                    }
                }
            }
        }
    }

Results
size (of each array dimension)
try → ms for the try catch implementation
if → ms for the if{} implementation

Test 1:
100
try → 1.73
if → 0.94

500
try → 15.87
if → 18.50

1000
try → 50.61
if → 73.77

5000
try → 1063.62
if → 1818.01

10000
try → 4103.19
if → 7226.37

Test 2
100
try → 41.46
if → 23.17

500
try → 303.68
if → 579.31

1000
try → 867.79
if → 2325.95

5000
try → 14197.22
if → 59067.40

This seems to show it is much better to use the try catch for performance reasons when there is a very large range of iterations with a low chance of an error (despite the conventions I have always been taught)

Interested to hear thoughts/insight from more experienced coders, in particular any additional considerations that may offset the performance increase.

Cheers.

2 Likes

I have no idea which is supposed to be faster in which situation, but I wonder, since your possibly creating exceptions/errors by doing the try catch, would it slow down performance in a situation where there are bad values and it goes out of range more frequently? Since that like, would try and run the exception handling code?

Anyway yeah I think your results are interesting, and I’ve never thought of doing something like that (the try catch with no exception handling), but could it have some pitfalls I wonder…

In the 1000x1000 case the if statement will make 1000000 checks (same for both tests) and the try catch will catch 4000 exceptions in test 1 and 12000 exceptions in test 2; the results seem to show that this ratio at least favours the try catch…

Test 1 with 100x100 is 10000 if statements vs 400 exceptions and Test 2 was 10000 if vs 1600 exceptions and in these cases the if statement was faster.

It appears somewhere between 100x100 and 500x500 the try catch performance overtakes the if statements. Somewhere around (very approximately) 0.1% or less error chance seems to be the approximate threshold where try catch becomes preferred.

1 Like

One problem with the if check is using Array.GetLength repeatedly, which should be cached instead. Also, not directly related, but in general 2D arrays are a fair amount slower than using a 1D array as a 2D array (i.e., foo[5, 7] would be foo[5 + 7*width]). Although jagged arrays are nearly as fast as 1D arrays (foo[5][7]).

–Eric

1 Like

I’ve got a similar system. I ended up building a custom indexer that wraps the array. That way I don’t have to do a bounds check in the calling code. The indexer simply clamps the index to the cached max and min values.

Probably not good for performance, but a huge boost for productivity.

Yea that was my bad, just doing up a quick test (even the map generation I just left it as 2D) but an interesting flow on from that is the try catch would actually be even better in that instance as the out of bounds exceptions would be reduced to a fraction (instead of outside the whole ‘square’ of the 2D array it would just be outside the bottom left and top right).

I do wonder in that case though if the row calculations (as per your eg. foo[5 + 7*width], one addition and one multiplication) every operation would actually make it slower when processing 1000^2 operations though…

Nope, I’ve worked with 2D arrays lots, and the math operations are basically irrelevant. Or at least, quite a bit faster than the bounds checking or whatever it is that slows down “real” 2D array access…CPUs are stupidly fast at adding and multiplying, not as much when doing branching operations.

–Eric

1 Like

So after a few more tests some interesting results…
Caching as expected resulted in some notable improvements (~20% faster over 1000x1000)
1D vs 2D and if vs try catch… will let the results tell the story.

Code

    public int size;
    public int[,] arrayTest;
    public int[] arrayTestAlt;
    int temp;

    void Awake ()
    {
        arrayTest = new int[size, size];
        arrayTestAlt = new int[size * size];
    }

    void Start()
    {
        StartCoroutine(DoTest());
    }



    IEnumerator DoTest()
    {
        yield return new WaitForSeconds(1);
        CheckTry();
        yield return new WaitForSeconds(1);
        CheckTryAlt();
        yield return new WaitForSeconds(1);
        CheckIfAlt();
        yield return new WaitForSeconds(1);
        Debug.Break();
    }

    void CheckTry()
    {
        for (int x = 0; x < size; x++)
        {
            for (int y = 0; y < size; y++)
            {
                try
                {
                    temp = arrayTest[x - 1, y];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTest[x + 1, y];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTest[x, y-1];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTest[x, y+1];
                }
                catch (System.Exception e) { }

            }
        }
    }

    void CheckTryAlt()
    {

        for (int x = 0; x < size; x++)
        {
            for (int y = 0; y < size; y++)
            {
                try
                {
                    temp = arrayTestAlt[x - 1 + (y * size)];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTestAlt[x + 1 + (y * size)];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTestAlt[x + ((y - 1) * size)];
                }
                catch (System.Exception e) { }
                try
                {
                    temp = arrayTestAlt[x + ((y + 1) * size)];
                }
                catch (System.Exception e) { }

            }
        }
    }

    void CheckIfAlt()
    {

        for (int x = 0; x < size; x++)
        {
            for (int y = 0; y < size; y++)
            {
                if (x > 0)
                {
                    temp = arrayTestAlt[x - 1 + (y * size)];
                }
                if (y > 0)
                {
                    temp = arrayTestAlt[x + ((y-1) * size)];
                }
                if (x < size - 1)
                {
                    temp = arrayTestAlt[x + 1 + (y * size)];
                }
                if (y < size - 1)
                {
                    temp = arrayTestAlt[x + ((y+1) * size)];
                }
            }
        }
    }

Results
With the cached array size instead of GetLength(), the three following options were tested:
2D array using try catch
1D array using try catch
1D array using if{}
Square int arrays of 500, 1000, 5000, and 10000 squared were tested.

500x500
2D array using try catch → 10.91
1D array using try catch → 7.21
1D array using if{} → 5.77

1000x1000
2D array using try catch → 33.55
1D array using try catch → 28.57
1D array using if{} → 24.32

5000x5000
2D array using try catch → 671.33
1D array using try catch → 731.01
1D array using if{} → 790.86

10000x10000
2D array using try catch → 2592.13
1D array using try catch → 3073.50
1D array using if{} → 3377.73

The interesting part there is from the 5000x5000 array onwards, it is actually faster to use a 2D array than a 1D array! (25 million math operations evidently add up!)

Conclusions
So the takeaways I have:

  • 1D arrays for general uses are better, as is the bounds testing using an if statement (As per generally accepted programming conventions)
  • As the array size increases to over a few million, it becomes more efficient to use both a 2D array AND a try catch block in lieu of an if statement.
    Rationale:
    The massive amount of operations (math operations and simple ‘if’ checks) while incredibly fast, start compounding as the array size (squared) gets larger so doing millions of fast operations add up.
    The try catch overhead from out of bounds exceptions becomes less and less relevant as the proportion of exceptions becomes less and less; thus a relatively small number of exceptions are handled compared to the millions of math operations / if statements
    Regarding the 2D array becoming more efficient - I am not entirely sure. This occurs around the same time so may be related to the above point regarding sheer number of math operations.

Interesting…
(Note this is a pretty specific scenario; where the entire array is iterated over in one hit with some simple operations undertaken on neighbouring indexes… As per the first conclusion point, this is unlikely to apply to most scenarios)

2 Likes

I optimized your CheckIfAlt function:

    void CheckIfAlt()
    {
        int max = size-1;
        for (int y = 0; y < size; y++)
        {
            int ySize = y*size;
            int yDown = (y-1)*size;
            int yUp = (y+1)*size;
            for (int x = 0; x < size; x++)
            {
                if (x > 0)
                {
                    temp = arrayTestAlt[x - 1 + ySize];
                }
                if (y > 0)
                {
                    temp = arrayTestAlt[x + yDown];
                }
                if (x < max)
                {
                    temp = arrayTestAlt[x + 1 + ySize];
                }
                if (y < max)
                {
                    temp = arrayTestAlt[x + yUp];
                }
            }
        }
    }

(This is why I always do the y loop before the x loop.)

–Eric

3 Likes

I did some tuning.

Turns out that specifying that you’re catching an object you’re not using has a stupidly high cost. Replacing each instance of this:

catch (System.Exception e) {}

with this:

catch () {}

Reduced my 1000x1000 time for two dimensions from ~0.026 to ~0.021 and for one dimension from ~0.023 to ~0.0175

So very approximately, 20% of the time was being taken up handling those Exception objects. There’s also a big GC cost. Those Exceptions are allocated when they’re generated.

Also note what Eric is doing. When you’re iterating big enough arrays, cache misses start costing a lot. Iterating linearly through the array instead of jumping around saves a lot of time. For my checks, the result for 10k * 10k after removing the caught Exceptions and using Eric’s loops:

Two dimensions, exceptions: 2.1 secs
One dimension, exceptions: 1.41 secs
One dimension, ifs: 1.5 secs

4 Likes

Cheers all, some interesting stuff.

That point re: pre caching the y calculations is interesting… plainly obvious in hindsight but a good reinforcement of optimisation. The point I made originally about the calculations was doing it in my head the way I implemented it. Very obvious (quantitative) efficiency improvements that way; appreciate the tip!
Optimisation is not something I have done a lot of but getting my head around more and more now. (Am moving on from the ‘get it working’ to the ‘get it working fast’ approach)

Regarding the Exception catching code; that is interesting. My understanding is that an exception object will be created regardless and must be handled or further thrown. I guess your code here is effectively a short hand for catch ANY exception? The time saving then would come from removal of the ‘is a’ check in my original code?

I very much appreciate the feedback thus far; an interesting topic and some good learning points reinforced. Still impressed that the try catch is more efficient in some circumstances.

Out of interest, final results based off methods discussed to improve performance.
A - > original code

    void CheckTry()
    {
        for (int x = 0; x < arrayTest.GetLength(0); x++)
        {
            for (int y = 0; y < arrayTest.GetLength(1); y++)
            {
                try
                {
                    temp = arrayTest[x - 1, y];
                }
                catch (System.IndexOutOfRangeException e) { }
                try
                {
                    temp = arrayTest[x + 1, y];
                }
                catch (System.IndexOutOfRangeException e) { }
                try
                {
                    temp = arrayTest[x, y-1];
                }
                catch (System.IndexOutOfRangeException e) { }
                try
                {
                    temp = arrayTest[x, y+1];
                }
                catch (System.IndexOutOfRangeException e) { }

            }
        }
    }

B → Optimised code

    void CheckTryAlt()
    {
        int max = size - 1;
        int ySize, yDown, yUp;
        for (int y = 0; y < size; y++)
        {
            ySize = y * size;
            yDown = (y - 1) * size;
            yUp = (y + 1) * size;

            for (int x = 0; x < size; x++)
            {
                try
                {
                    temp = arrayTestAlt[x - 1 + ySize];
                }
                catch (System.IndexOutOfRangeException e) { }
                try
                {
                    temp = arrayTestAlt[x + 1 + ySize];
                }
                catch (System.IndexOutOfRangeException e) { }
                try
                {
                    temp = arrayTestAlt[x + yDown];
                }
                catch (System.IndexOutOfRangeException e) { }
                try
                {
                    temp = arrayTestAlt[x + yUp];
                }
                catch (System.IndexOutOfRangeException e) { }

            }
        }
    }

Note the exception was specified as an index out of range as opposed to Baste’s suggestion as I realised that by catching all exceptions, there may be an issue with the processing that is unrelated to the indexing which I do NOT want to catch here.

Results
1000x1000
A : 49.30ms
B : 20.04ms

10000x10000
A : 3997.31ms
B : 1607.72ms

Pretty damn good improvement from caching and 1D array over 2D. Cheers for all the points in this thread eh.

2 Likes

Another thread just referenced this thread so I thought it would be fun to try the JobSystem and Burst compiler using IJobFor (ie main thread only). I believe I’ve applied all of the most up-to-date snippets. It’s been several months since I last played with DOTS so hopefully I didn’t butcher the test itself.

Line 57 is the first line for setting up and starting the test, and line 73 is the first line of the IJobFor.

Full Code

using System.Collections;
using System.Diagnostics;
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;

public class PerformanceTest : MonoBehaviour
{
    public int size;
    private int[,] arrayTest;
    private int[] arrayTestAlt;
    private int temp;

    private Stopwatch stopwatch;

    void Awake()
    {
        arrayTest = new int[size, size];
        arrayTestAlt = new int[size * size];
        stopwatch = new Stopwatch();
    }

    void Start()
    {
        StartCoroutine(DoTest());
    }

    IEnumerator DoTest()
    {
        yield return new WaitForSeconds(1);

        stopwatch.Start();
        CheckTry();
        stopwatch.Stop();
        UnityEngine.Debug.Log($"Try completed in: {stopwatch.ElapsedMilliseconds} ms");
        stopwatch.Reset();

        yield return new WaitForSeconds(1);

        stopwatch.Start();
        CheckTryAlt();
        stopwatch.Stop();
        UnityEngine.Debug.Log($"TryAlt completed in: {stopwatch.ElapsedMilliseconds} ms");
        stopwatch.Reset();

        yield return new WaitForSeconds(1);

        stopwatch.Start();
        CheckIfAlt();
        stopwatch.Stop();
        UnityEngine.Debug.Log($"IfAlt completed in: {stopwatch.ElapsedMilliseconds} ms");
        stopwatch.Reset();

        yield return new WaitForSeconds(1);

        NativeArray<int> nativeArray = new NativeArray<int>(arrayTestAlt, Allocator.TempJob);
 
        stopwatch.Start();
        JobIfAlt job = new JobIfAlt { size = size, array = nativeArray };
        JobHandle handle = job.Schedule(size * size, default(JobHandle));
        handle.Complete();
        stopwatch.Stop();
 
        nativeArray.Dispose();
        UnityEngine.Debug.Log($"JobIfAlt completed in: {stopwatch.ElapsedMilliseconds} ms");

        yield return new WaitForSeconds(1);

        UnityEditor.EditorApplication.isPlaying = false;
    }

    [BurstCompile]
    struct JobIfAlt : IJobFor
    {
        public int size;
        public NativeArray<int> array;

        public void Execute(int index)
        {
            int x = index % size;
            int y = index / size;

            if (x > 0)
            {
                array[index] = array[x - 1 + (y * size)];
            }
            if (y > 0)
            {
                array[index] = array[x + ((y - 1) * size)];
            }
            if (x < size - 1)
            {
                array[index] = array[x + 1 + (y * size)];
            }
            if (y < size - 1)
            {
                array[index] = array[x + ((y + 1) * size)];
            }
        }
    }

    void CheckTry()
    {
        for (int x = 0; x < arrayTest.GetLength(0); x++)
        {
            for (int y = 0; y < arrayTest.GetLength(1); y++)
            {
                try
                {
                    temp = arrayTest[x - 1, y];
                }
                catch (System.IndexOutOfRangeException) { }
                try
                {
                    temp = arrayTest[x + 1, y];
                }
                catch (System.IndexOutOfRangeException) { }
                try
                {
                    temp = arrayTest[x, y-1];
                }
                catch (System.IndexOutOfRangeException) { }
                try
                {
                    temp = arrayTest[x, y+1];
                }
                catch (System.IndexOutOfRangeException) { }

            }
        }
    }

    void CheckTryAlt()
    {
        int max = size - 1;
        int ySize, yDown, yUp;
        for (int y = 0; y < size; y++)
        {
            ySize = y * size;
            yDown = (y - 1) * size;
            yUp = (y + 1) * size;

            for (int x = 0; x < size; x++)
            {
                try
                {
                    temp = arrayTestAlt[x - 1 + ySize];
                }
                catch (System.IndexOutOfRangeException) { }
                try
                {
                    temp = arrayTestAlt[x + 1 + ySize];
                }
                catch (System.IndexOutOfRangeException) { }
                try
                {
                    temp = arrayTestAlt[x + yDown];
                }
                catch (System.IndexOutOfRangeException) { }
                try
                {
                    temp = arrayTestAlt[x + yUp];
                }
                catch (System.IndexOutOfRangeException) { }

            }
        }
    }

    void CheckIfAlt()
    {
        int max = size-1;
        for (int y = 0; y < size; y++)
        {
            int ySize = y*size;
            int yDown = (y-1)*size;
            int yUp = (y+1)*size;
            for (int x = 0; x < size; x++)
            {
                if (x > 0)
                {
                    temp = arrayTestAlt[x - 1 + ySize];
                }
                if (y > 0)
                {
                    temp = arrayTestAlt[x + yDown];
                }
                if (x < max)
                {
                    temp = arrayTestAlt[x + 1 + ySize];
                }
                if (y < max)
                {
                    temp = arrayTestAlt[x + yUp];
                }
            }
        }
    }
}

1000x1000
Try: 22ms
TryAlt: 8ms
IfAlt: 8ms
JobIfAlt: 9ms

10000x10000
Try: 2164ms
TryAlt: 851ms
IfAlt: 828ms
JobIfAlt: 700ms

4 Likes