Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

Untity Synchronization context + Task.Run huge performance hit!

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

  1. RaventurnPatrick

    RaventurnPatrick

    Joined:
    Aug 9, 2011
    Posts:
    36
    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):
    1.  
    2. public class SynchronizationContextTestBehaviour : MonoBehaviour
    3. {
    4.   private TaskScheduler _mts;
    5.  
    6.   private void Awake()
    7.   {
    8.     _mts = TaskScheduler.FromCurrentSynchronizationContext();
    9.     PerfTest();
    10.   }
    11.  
    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) + ")");
    19.  
    20.     sw = Stopwatch.StartNew();
    21.     SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
    22.     await CreateGameObjects("NetStandard");
    23.     Debug.Log("Elapsed (.net default): " + sw.Elapsed.TotalMilliseconds + "ms");
    24.   }
    25.  
    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.   }
    41.  
    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 task.run 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

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    4,213
    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

    RaventurnPatrick

    Joined:
    Aug 9, 2011
    Posts:
    36
    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)