jobs in coroutine = poor performance?

I’m starting to wonder if jobs are usable in coroutine, have a look at this profiler, that’s a LOT in the main thread.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class JobSimple : MonoBehaviour {

    struct FillTheArray : IJobParallelFor
    {
        public NativeArray<float> output;

        public void Execute(int i)
        {
            output[i] = Mathf.Log10( i);
        }
    }

    struct CalculateThings : IJobParallelFor
    {
        [ReadOnly]
        public NativeArray<float> input;

        public NativeArray<float> output;

        public void Execute(int i)
        {
            output[i] = Mathf.Sin( input[i]);
        }
    }

    void OnEnable()
    {
        StartCoroutine(JobCompute());
    }

    public int computeSize=1000000, batchSize = 100;
    NativeArray<float> input, output;
    JobHandle handleCalculate;
    IEnumerator JobCompute()
    {
        while (true)
        {
            input = new NativeArray<float>(computeSize, Allocator.Persistent);
            output = new NativeArray<float>(computeSize, Allocator.Persistent);

            var time = Time.time;

            var jobFiller = new FillTheArray()
            {
                output = input
            };
            var handleFiller = jobFiller.Schedule(computeSize, batchSize);

            var job = new CalculateThings()
            {
                input = input,
                output = output
            };
            handleCalculate = job.Schedule(input.Length, batchSize, handleFiller);
            yield return new WaitWhile(() => handleCalculate.IsCompleted);
            handleCalculate.Complete();
            Debug.Log(Time.time - time);
            input.Dispose();
            output.Dispose();
        }
    }

    void OnDisable()
    {
        handleCalculate.Complete();
        input.Dispose();
        output.Dispose();
    }
}
1 Like
yield return new WaitWhile(() => handleCalculate.IsCompleted);

Looks like the condition is inverted? Shouldn’t it be this instead?

yield return new WaitWhile(() => !handleCalculate.IsCompleted);

HAHAHAHHA - jeez thanks!

err… when the thread is done I see some spikes here, any idea?

Looks like it’s spending time in your function. You could try putting in some profiler blocks.

I ran a deep profiler because with profiler blocks don’t show up in the profiler. Might be a coroutine thing, maybe time too short to show up?

it’s CustomYieldInstruction that’s taking time which is handleCalculate.IsCompleted

maybe try

while (!handleCalculate.IsCompleted) {

    yield return null;
}

First a general comment…
Why are you using a coroutine for this specifically?
It seems to me that:

  • Complicates the code
  • Makes intent less clear & readable
  • allocates GC memory
    an Update() method with
    if (!jobHandle.IsCompleted())
    return;

Will do just fine… keep it simple… (Rant over…)

For the most likely cause of the spike when the job completes, you are scheduling a new iteration.
For this you are allocating a two new NativeArray with 1000000 elements. This means we

  1. allocate 4mb of memory
  2. clear all that memory with zeros by default

Allocating 4mb of memory is relatively cheap (compared to your 7ms spike - the size of the allocated memory generally doesn’t make it significantly more expensive)

However clearing 4mb of memory to zeros on my laptop takes around 1-2 ms for each array. (My laptop has a bandwidth per core of around 3GB/second) Fortunately there is an option on NativeArray where you can tell it to leave the memory uninitialized:
See NativeArrayOptions on the constructor. So essentially you allocate on main thread and then fill the memory on job. Skipping the cost of 2 on the main thread.

Naturally if you are not carefully and don’t gurantee to write to every element before reading them in your code you get randomly initialized memory.

Its not clear what your code is trying to achieve so potentially keeping the native array around instead of allocating it on each iteration would be desirable?

9 Likes

Btw. did you know about
Profiler.Begin(“Some block”);

Profiler.End();

Usually when i try to find out why something is slow, i put those around all the different places that might cause the performance hit. With coroutines you need to be careful not to cross yield boundaries with begin / end.

6 Likes

Can someone tell me why everyone is using Allocator.Persistent instead of Allocator.Temp, like in video that shows us jobsystem?

My best guess would be “lack of documentation”, which I expect to change, once UT officially announces the release of its new tech and provides all the documentation one needs to make the best use of it.

1 Like

Because I’m lazy.
This example is silly but in the real thing I set up the jobs, schedule them then wait on the job to complete and after that I work on the result. For this sort of linear progression that yield to a result, coroutines are just easier.
In an update I would have to break it down in two if blocks, one for jobRunning==false (because a handle doesn’t have progress), the other one for handle.isCompleted.

Could we get a status on the handle? I’m thinking handle.progress, To mirror AsyncOperation. And even if calculating progress isn’t trivial (well… for the JobFor it is) jumping from 0 to 1 is good enough.

Also it would be great if you could rename handle.isCompleted to handle.isDone so it matches AsyncOperation.isDone, because I notice that the Unity API has many different words for the same thing and it is not ideal. (request.done I am looking at you)

I got rid of the allocation and now it is smooth - I have questions dependencies so I’ll open a new thread.

NativeArrayOptions.None… brilliant!

True story… not sure how NativeArrayOptions.None happened, but API has been fixed for beta 5.

1 Like

I am not sure I can agree. Done is slightly a different meaning to Completed. There is also the tense to worry about like wasCompleted vs isComplete and wasDone vs isDone.

I would probably recommend using .done or .finished instead.

Every API has some broken English in it though, not just Unity :slight_smile:

So it would make sense to use either .done or .finished property for everything if everyone is looking for consistency across different parts of the API and doesn’t mind so much about chronological accuracy. I’m sure if we went down this rabbit hole then the work would never be isDone or isCompleted.

I’m done! All finished!

Experimenting with Jobs inside of coroutines myself. It’s a messy place to be, especially when you nest multiple coroutines. Thanks for sharing your example.

Working on a long-running IENumerator Start that does procedural world generation, which reports back to a loading screen as it goes. Wanted to speed it up, so users won’t have to wait so long for the world to generate. I looked to Jobs for help.

I like the pattern of schedule, yield wait while, then completed.

Regarding the Jobs system, FWIW, it seems easy to make mistakes regarding marshaling data back and forth without getting clear feedback you’re doing it wrong.

And sometimes it’s silence regarding the mistakes I was making meant I couldn’t differentiate between misallocated data and the variations programmed into the procedural generation.

1 Like