Single Thread Vs Tasks Vs Unity C# Jobs – Simple Performance Test

I have recently been looking into Unity’s Data-Orientated Technology Stack as it is fairly new and my commercial experience so far has been with the current bog-standard Unity.

I decided I would do some code that would just crunch through numbers, no Unity API.

I would test the following:

The following was the “expensive function” I would test:

   public static void DumbTest()
    {
        float value = 0f;
        for (int i = 0; i < 60000; ++i)
        {
            value += Mathf.Exp(i) * Mathf.Sqrt(value);
        }
    }

The function would be called using this MonoBehaviour:

using System.Diagnostics;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;
using Debug = UnityEngine.Debug;
using System.Threading.Tasks;
using Unity.Burst;

public class JobsTest : MonoBehaviour
{
    [SerializeField]
    private int _iterations;

    private enum Method
    {
        Standard,
        Tasks,
        JobsArray
    }

    [SerializeField]
    private Method _method;

    private bool _run = false;

    private readonly Stopwatch _stopWatch = new Stopwatch();

    private NativeArray<JobHandle> _nativeHandles;
    private Task[] _tasks;

    private void Update()
    {
        if (!_run)
        {
            _nativeHandles = new NativeArray<JobHandle>(
                          _iterations, Allocator.Temp);
            _tasks = new Task[_iterations];

            switch (_method)
            {
                case Method.Standard:
                    {
                        _stopWatch.Start();
                        for (int i = 0; i < _iterations; ++i)
                        {
                            DumbTest();
                        }
                        _stopWatch.Stop();
                        Debug.Log(_method.ToString() + " " + 
                                   _stopWatch.ElapsedMilliseconds + "ms");
                        break;
                    }
                case Method.Tasks:
                    {
                        _stopWatch.Start();
                        for (int i = 0; i < _iterations; ++i)
                        {
                            _tasks[i] = Task.Factory.StartNew(DumbTest);
                        }
                        Task.WaitAll(_tasks);
                        _stopWatch.Stop();
                        Debug.Log(_method.ToString() + " " + 
                                    _stopWatch.ElapsedMilliseconds + "ms");
                        break;
                    }
                case Method.JobsArray:
                    {
                        _stopWatch.Start();
                        for (int i = 0; i < _iterations; ++i)
                        {
                            _nativeHandles[i] = CreateJobHandle();
                        }
                        JobHandle.CompleteAll(_nativeHandles);
                        _stopWatch.Stop();
                        Debug.Log(_method.ToString() + " " + 
                                   _stopWatch.ElapsedMilliseconds + "ms");
                        break;
                    }
            }
        }
        _run = true;
    }

    public JobHandle CreateJobHandle()
    {
        var job = new TestJob();
        return job.Schedule();
    }

    //[BurstCompile]
    public struct TestJob : IJob
    {
        public void Execute()
        {
            DumbTest();
        }
    }

    public static void DumbTest()
    {
        float value = 0f;
        for (int i = 0; i < 60000; ++i)
        {
            value += Mathf.Exp(i) * Mathf.Sqrt(value);
        }
    }
}

I ran the code over 100 iterations, 3 times and got the following results:

123
Single-Threaded1506ms1740ms1437ms
Tasks 419ms444ms397ms
Jobs434ms428ms480ms

Looking at the above results, on initial observation, to crunch through numbers, tasks are actually faster.

However, one thing to note is that I had not enabled the Burst Compiler.

If I add the Burst compiler flag to my Job:

  [BurstCompile]
    public struct TestJob : IJob
    {
        public void Execute()
        {
            DumbTest();
        }
    }

Here were the results

123
Jobs With Burst39ms40ms41ms

As you can see there is a massive performance increase.

In conclusion then, based on the above tests, if you are going to crunch through work without the Burst Compiler, maybe look at using something like Tasks first before opting for Jobs. If you are going to use the Burst Compiler, use the Job System.

Here are the specs of the machine I tested on

Processor: Intel(R) Xeon(R) CPU E31225 @ 3.10GHz (4 CPUs), ~3.1GHz
Memory: 32768MB RAM
GFX Card: NVIDIA GeForce GTX 960