Search Unity

Untity Synchronization context + Task.Run huge performance hit!

Discussion in 'Scripting' started by RaventurnPatrick, Sep 5, 2019.

  1. RaventurnPatrick


    Aug 9, 2011
    Hey i did some testing with untiys default synchronization context. For some reasons it is way slower than the .net default task scheduler. Each await Task.Run causes a one frame delay!

    Here a simple test behaviour that creates 1000 game objects
    Yes it's an artificial example, but it can be useful if each game object needs procedural data that is generated in the task before the main thread scheduler creates the game object ;)

    Code (CSharp):
    2. public class SynchronizationContextTestBehaviour : MonoBehaviour
    3. {
    4.   private TaskScheduler _mts;
    6.   private void Awake()
    7.   {
    8.     _mts = TaskScheduler.FromCurrentSynchronizationContext();
    9.     PerfTest();
    10.   }
    12.   private async Task PerfTest()
    13.   {
    14.     var frame = Time.frameCount;
    15.     var sw = Stopwatch.StartNew();
    16.     await CreateGameObjects("UnityDefault");
    17.     Debug.Log("Elapsed (unity default): " + sw.Elapsed.TotalMilliseconds + "ms (frames: " +
    18.               (Time.frameCount - frame) + ")");
    20.     sw = Stopwatch.StartNew();
    21.     SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
    22.     await CreateGameObjects("NetStandard");
    23.     Debug.Log("Elapsed (.net default): " + sw.Elapsed.TotalMilliseconds + "ms");
    24.   }
    26.   private async Task CreateGameObjects(string prefix)
    27.   {
    28.     for (int i = 0; i < 1000; i++)
    29.     {
    30.       await Task.Run(() =>
    31.       {
    32.         var number = i;
    33.         Task.Factory.StartNew(() =>
    34.         {
    35.            var go = new GameObject(prefix + number);
    36.            go.transform.parent = transform;
    37.         }, CancellationToken.None, TaskCreationOptions.None, _mts);
    38.       });
    39.     }
    40.   }
    Elapsed (unity default): 6475,0165ms (1000 frames)
    Elapsed (.net default): 22,5575ms

    with 10_000 gos:
    Elapsed (unity default): 47214,6553ms (10000 frames)
    Elapsed (.net default): 164,49ms

    With the unity default scheduler it seems only 1 task per frame is run as you can clearly see in the increasing elapsed frame count directly proportional to the count of calls we have

    Has anybody more insight into this behaviour? Is this expected? I would assume the synchronization context is implemented in the wrong way?
  2. Kurt-Dekker


    Mar 16, 2013
    Haven't used these APIs but you can make your own thread synchronization by simply creating delegates on the Task/Thread side (i.e., put each block of Unity-API-facing code into its own System.Action variable and add it to a list).

    Then each frame in your Update() you can check if there is anything in this list, remove the actions to a fresh temp list, and go execute all of those System.Action calls, which will obviously be done on the main thread.

    Don't forget to lock() the list on the add and the remove sides!!
  3. RaventurnPatrick


    Aug 9, 2011
    Yep that's definetly a valid approach.
    I just like c# async await api and the other powerful features of the TPL. I would just hope that Unity improves the builtin synchronization context and adds support for stuff like time-slicing, instead of executing just one action per frame.

    I resolved the problems by just scheduling tasks with .ConfigureAwait(false) at the end (thus the calling method continues in a background thread and schedules further tasks immediatly instead of waiting on update cycle), but this is not always possible if the caller needs to return to the main thread (and still wait for a task result)