Search Unity

Question It's ok to change data in the static List from jobs?

Discussion in 'C# Job System' started by hpr895, Mar 24, 2024.

  1. hpr895

    hpr895

    Joined:
    Apr 30, 2019
    Posts:
    8
    Hello. Documentation says "Do not access static data from a job". But my function changes only one unit with their index and does not affect others. Creating NativeArrays, combining data and disposing they is way more expensive so I don't want to use them.

    (For 1,000,000 units with NativeArrays it's 20FPS, and with static List direct changing is 150FPS)

    So is this NORMAL to use the static list directly in that kind of situations?

    Example:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Unity.Jobs;
    5.  
    6. public class Unit
    7. {
    8.     public float positionX;
    9.     public float speed;
    10. }
    11.  
    12. public class JobsTest : MonoBehaviour
    13. {
    14.     static List<Unit> units = new List<Unit>();
    15.  
    16.     private void Start()
    17.     {
    18.         for (int i = 0; i < 1000; i++) {
    19.             units.Add(new Unit() {
    20.                 positionX = 0,
    21.                 speed = Random.Range(0.1f, 0.2f),
    22.             });
    23.         }
    24.     }
    25.  
    26.     private void Update()
    27.     {
    28.         MyJob jobData = new MyJob();
    29.         JobHandle handle = jobData.Schedule(units.Count, 100);
    30.         handle.Complete();
    31.     }
    32.  
    33.     private struct MyJob : IJobParallelFor
    34.     {
    35.         public void Execute(int i)
    36.         {
    37.             units[i].positionX += units[i].speed;
    38.         }
    39.     }
    40. }
    41.  
     
    Last edited: Mar 24, 2024
  2. hpr895

    hpr895

    Joined:
    Apr 30, 2019
    Posts:
    8
    Example with "slow" native arrays:

    Code (CSharp):
    1. private void Update()
    2. {
    3.     NativeArray<float> positions = new NativeArray<float>(units.Count, Allocator.TempJob);
    4.     NativeArray<float> speed = new NativeArray<float>(units.Count, Allocator.TempJob);
    5.     NativeArray<float> result = new NativeArray<float>(units.Count, Allocator.TempJob);
    6.  
    7.     for (int i = 0; i < units.Count; i++) {
    8.         positions[i] = units[i].positionX;
    9.         speed[i] = units[i].speed;
    10.     }
    11.  
    12.     MyJob jobData = new MyJob {
    13.         positions = positions,
    14.         speed = speed,
    15.         result = result,
    16.     };
    17.  
    18.     JobHandle handle = jobData.Schedule(units.Count, 100);
    19.     handle.Complete();
    20.  
    21.     for (int i = 0; i < units.Count; i++) {
    22.         units[i].positionX = result[i];
    23.     }
    24.  
    25.     positions.Dispose();
    26.     speed.Dispose();
    27.     result.Dispose();
    28. }
    29.  
    30. private struct MyJob : IJobParallelFor
    31. {
    32.     [ReadOnly]
    33.     public NativeArray<float> positions;
    34.  
    35.     [ReadOnly]
    36.     public NativeArray<float> speed;
    37.  
    38.     [NativeDisableParallelForRestriction]
    39.     public NativeArray<float> result;
    40.  
    41.     public void Execute(int i)
    42.     {
    43.         result[i] = positions[i] + speed[i];
    44.     }
    45. }
    46.  
     
  3. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,782
    First of all, don't use list. Use NativeList if you can not use NativeArray.

    Then you pass reference into a job, from an update. Or whenever job is called.
    You can also declare it as read only.
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,025
    You cannot use managed types in a job. List<T> is a managed type. It doesn‘t matter how you access it.
     
  5. Spy-Master

    Spy-Master

    Joined:
    Aug 4, 2022
    Posts:
    643
    That’s not quite right, or you’re being too vague. It’s better to be more precise. If a job isn’t Burst-compiled, as is the case here, you can use any managed code you like, including method-local / static managed objects like lists and strings. You just can’t have a managed object as a job field, since the job struct itself needs to be unmanaged.
     
    CodeSmile likes this.
  6. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,025
    Actually I did not know that. Not burst compiling jobs is pretty pointless though, leaves too much performance untapped even considering having to copy from list to native list in many cases. ;)
     
    hpr895 likes this.
  7. hpr895

    hpr895

    Joined:
    Apr 30, 2019
    Posts:
    8
    Thank you for this non-obvious detail!

    I found something from the Burst docs:
    Projects that don't use Burst:
    • iOS projects from the Windows Editor
    • Android projects from the Linux Editor
    • Xcode projects generated from the Create Xcode Project option
    I need iOS platforms, so I decide to keep my code single threaded. Anyway units logic AI is hard to make multithreaded. For games with physics and simulations - ok, but not deep logic.

    But server headless build can use burst in that situation :)

    The problem root of this optimization fever is about it's cheaper to rent one multithreaded server then lot of single-CPU for multiplayer games hosting.
     
    Last edited: Mar 28, 2024
  8. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,025
    The code will still be bursted if you build on Mac (just not on Windows), and without the „create xcode project“ option.
     
  9. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,025
    A single core CPU won‘t cut it as a game server. Unity has internal multihreaded processes and you don‘t want server hiccups due to OS scheduling tasks that block the only CPU every now and then.
     
  10. Spy-Master

    Spy-Master

    Joined:
    Aug 4, 2022
    Posts:
    643
    You don’t technically have to constrain yourself to single-threaded stuff just because of that. The job system is literally based on utilizing worker threads to distribute work. You can have parallel jobs based on either normal managed code or Burst, and parallel is typically better than single-thread. Especially on iOS, where you’re using IL2CPP as opposed to Mono, the performance difference between Burst and the alternative won’t be quite as bad, but still, if the work could be compiled with Burst, it should. Unfortunate to hear that’s not an option. Down the line, you might consider finding some secondhand macOS device with sufficient storage for Xcode, the Unity editor, and your project as a platform for building for iOS. Alternatively, Unity cloud build has macOS build hosts as a premium (not free) option.

    With regard to the original comparison you draw:
    The parallel job using a static list is “faster” because you don’t have the separate single-threaded setup and integration steps at the start and end for the work you do with the NativeArrays, which end up overshadowing whatever benefit parallelizing the middle would have. If your Unit type could instead be an unmanaged struct (which in this case would be true given it’s just some primitives), you would be able to use a NativeArray<Unit>/NativeList<Unit> with Allocator.Persistent (create it in Awake, dispose it in OnDestroy) and perform the whole update in the parallel job instead of having to set up inputs and integrate the results. Using static mutable data in jobs is pretty much an anti-pattern to be avoided, I would instead be looking at re-architecting the data in a manner friendly to jobs that reduces main-thread work while still being usable in other game code.
     
    hpr895 likes this.
  11. hpr895

    hpr895

    Joined:
    Apr 30, 2019
    Posts:
    8
    Wow! Persistent data really makes things different.

    Now I need to reindex data on unit deleting to save corellation between unit index in List<Unit> and data indexes in arrays, but all this system works incomparably faster. I broke my mind for years on this issue, thank you very much! :)

    But [BurstCompile] makes jobs slower.

    Threads code block now looks like this:
    Code (CSharp):
    1. List<Unit> units = new List<Unit>();
    2. NativeArray<float> unitsPosX;
    3. NativeArray<float> unitsSpeed;
    4.  
    5. private void Awake()
    6. {
    7.     unitsPosX = new NativeArray<float>(maxUnitsCount, Allocator.Persistent);
    8.     unitsSpeed = new NativeArray<float>(maxUnitsCount, Allocator.Persistent);
    9. }
    10.  
    11. private void OnDestroy()
    12. {
    13.     unitsPosX.Dispose();
    14.     unitsSpeed.Dispose();
    15. }
    16.  
    17. void MoveUnits()
    18. {
    19.     new UnitMoveJob() {
    20.         unitsPosX = unitsPosX,
    21.         unitsSpeed = unitsSpeed,
    22.     }.Schedule(units.Count, batchSize).Complete();
    23. }
    24.  
    25. public struct UnitMoveJob : IJobParallelFor
    26. {
    27.     public NativeArray<float> unitsPosX;
    28.     public NativeArray<float> unitsSpeed;
    29.  
    30.     public void Execute(int i)
    31.     {
    32.         unitsPosX[i] += unitsSpeed[i];
    33.     }
    34. }
    35.  
     
    Last edited: Mar 29, 2024
  12. hpr895

    hpr895

    Joined:
    Apr 30, 2019
    Posts:
    8
    Oops, sorry. :) Burst is faster, just needed to turn off the "Native Debug Mode Compilation" field in menu Jobs/Burst, and add [BurstCompile(DisableSafetyChecks = true)] parameter to the function. Just for history.

    And of course it's way better to use one array with structs then many arrays of primitives.Now it perform more then 10,000,000 units with good fps. In example project, of course.
     
    Last edited: Apr 1, 2024