Search Unity

Question Burst Compiled Job Optimization Help

Discussion in 'Entity Component System' started by Gabe851, Nov 3, 2022.

  1. Gabe851

    Gabe851

    Joined:
    May 13, 2017
    Posts:
    32
    Hello! I've recently converted a method that runs a foreach loop on update over to a Burst compiled Job, but I did not notice a performance improvement (I think I actually lost a few frames, and I don't see much difference in the compiler), but it's likely I'm not doing a very good job.

    I took inspiration from this Unite video, especially the part with the code starting at about 22 minutes:




    That video is a bit old, but I mean that in general I wanted to see if I could get performance improvements out of copying all my array of entities into native arrays, passing that with extra info into a job, running that job for each entity, and then passing the info back and copying it back into the original arrays.

    My implementation is below.

    First, the IJob stuff:



    Code (CSharp):
    1.  
    2. public struct CoordinateSoldierValues
    3.     {
    4.         public Vector3 soldierPosition;
    5.         public Vector3 destinationPosition;
    6.         public float soldierPathSpeed;
    7.     }
    8.  
    9.     [BurstCompile]
    10.     public struct CoordinateSoldierJob : IJob
    11.     {
    12.         [ReadOnly]
    13.         public NativeArray<CoordinateSoldierValues> soldiers;
    14.         public int count;
    15.         public Vector3 pivot;
    16.         public Quaternion targetRotation;
    17.         public Vector3 unitPositionMinusFormationCentreMinusPivot;
    18.         public float unitSpeed;
    19.  
    20.         [WriteOnly]
    21.         public NativeArray<float> outputPathSpeeds;
    22.         [WriteOnly]
    23.         public NativeArray<Vector3> outputPathDestinations;
    24.  
    25.         public void Execute()
    26.         {
    27.             CoordSoldierMethodForJob(ref soldiers, count, pivot, targetRotation, unitPositionMinusFormationCentreMinusPivot, unitSpeed, outputPathSpeeds, outputPathDestinations);
    28.         }
    29.     }
    30.  
    31.     public static void CoordSoldierMethodForJob(ref NativeArray<CoordinateSoldierValues> soldiers, int count, Vector3 pivot, Quaternion targetRotation,
    32.         Vector3 unitPositionMinusFormationCentreMinusPivot, float unitSpeed, NativeArray<float> outputPathSpeeds, NativeArray<Vector3> outputPathDestinations)
    33.     {
    34.         for(int i = 0; i < count; i++)
    35.         {
    36.             var desiredPosition = targetRotation * (soldiers[i].destinationPosition + unitPositionMinusFormationCentreMinusPivot) + pivot;
    37.             Vector3 currentPos = soldiers[i].soldierPosition;
    38.             var formationDiff = desiredPosition - currentPos;
    39.             var mag = formationDiff.sqrMagnitude;
    40.             float totalMag = mag;
    41.             float magScalar = totalMag / 50;
    42.             if(totalMag > 5.0f)
    43.             {
    44.                 outputPathSpeeds[i] = unitSpeed + unitSpeed * magScalar;
    45.             }
    46.             else if(mag > 1.0f)
    47.             {
    48.                 outputPathSpeeds[i] = unitSpeed + unitSpeed * magScalar * 0.5f;
    49.             }
    50.             else
    51.             {
    52.                 outputPathSpeeds[i] = Mathf.Min(soldiers[i].soldierPathSpeed, unitSpeed);
    53.             }
    54.             outputPathDestinations[i] = desiredPosition;
    55.         }
    56.     }


    and then where I actually use it:



    Code (CSharp):
    1.        
    2.  
    3.         float unitSpeed = _unit.CurrentMovementSpeed;
    4.         Vector3 unitPositionMinusFormationCentreMinusPivot = unitPositionMinusFormationCentre - _pivot;
    5.  
    6.         int soldierCount = _unit.Soldiers.Count;//this is what i'm iterating over
    7.         var arrayOfSoldiers = new NativeArray<CoordinateSoldierValues>(soldierCount, Allocator.TempJob);
    8.         var outputSpeeds = new NativeArray<float>(soldierCount, Allocator.Persistent);
    9.         var outputDestinations = new NativeArray<Vector3>(soldierCount, Allocator.Persistent);//should i use a different allocator? i'm not sure what is best for what
    10.  
    11.         for(int i = 0; i < soldierCount; i++)
    12.         {
    13.             Soldier thisSoldier = _unit.Soldiers[i];
    14.             arrayOfSoldiers[i] = new CoordinateSoldierValues
    15.             {
    16.                 destinationPosition = thisSoldier._finalDestination + thisSoldier.randomOffset,
    17.                 soldierPosition = thisSoldier.GetPosition(),
    18.                 soldierPathSpeed = thisSoldier.astarPath.speed
    19.             };
    20.         }
    21.  
    22.         var runJob = new CoordinateSoldierJob {
    23.             soldiers = arrayOfSoldiers,
    24.             count = soldierCount,
    25.             pivot = _pivot,
    26.             targetRotation = _targetRotation,
    27.             unitPositionMinusFormationCentreMinusPivot = unitPositionMinusFormationCentreMinusPivot,
    28.             unitSpeed = unitSpeed,
    29.             outputPathDestinations = outputDestinations,
    30.             outputPathSpeeds = outputSpeeds
    31.         };
    32.         runJob.Run();
    33.  
    34.         for(int i = 0; i < soldierCount; i++)
    35.         {
    36.             Soldier soldier = _unit.Soldiers[i];
    37.             soldier.astarPath.speed = outputSpeeds[i];
    38.             soldier.astarPath.destination = outputDestinations[i];
    39.         }
    40.         arrayOfSoldiers.Dispose();
    41.         outputDestinations.Dispose();
    42.         outputSpeeds.Dispose();
    43.  
    I know the above is probably pretty bad code, but is there anything in particular I need to change? The arrangement of the data, or the native arrays, or the type of allocator? Should I use unity mathematics somewhere?

    Thanks for any help.
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    Your job setup is O(n). The job is O(n). And your post-processing is O(n). The reason you don't see a speedup is because you are doing nearly as much work setting up and post-processing the job as you were doing the job outside of Burst. If the job itself had a larger algorithmic complexity, or if you could keep your data in an unmanaged form before and after the job to skip the setup and post-processing costs, then you would see a greater difference.
     
  3. Gabe851

    Gabe851

    Joined:
    May 13, 2017
    Posts:
    32
    thanks for the reply! in this last part, but unmanaged form, you mean like store all the soldiers in native arrays to begin with? or use ECS?
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    Exactly.

    If you need examples of either approach I will be more than happy to provide them.
     
    mbalmaceda likes this.
  5. Gabe851

    Gabe851

    Joined:
    May 13, 2017
    Posts:
    32
    thanks! I've been looking at the ecs samples project for .51, trying to spawn entities at runtime (I cannot convert in scene ones as the units are spawned not pre-existing), doing something like this on spawn:

    first initializing this stuff:

    Code (CSharp):
    1.  var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
    2.         var prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(soldierPrefab, settings);
    3.         var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    and then the foreach soldier:

    Code (CSharp):
    1.  for (int i = 0; i < amount; i++)
    2.         {
    3.             FormationSpot fs = formation.Spots[i];
    4.  
    5.             var ent = entityManager.Instantiate(prefab);
    6.             entityManager.SetComponentData(ent, new Translation { Value = fs.position });
    7.             entityManager.SetComponentData(ent, new Rotation { Value = Quaternion.LookRotation(fs.direction, Vector3.up) });
    8.  
    9.             var crowdPrefab = entityManager.GetComponentObject<GPUICrowdPrefab>(ent);
    10.  
    11. ...
    12. unit.AddSoldier(entityManager.GetComponentObject<Soldier>(ent));
    but I didn't quite understand the get/set component stuff for entities. SetComponentData on translation and rotation seemed to work, but the GetComponentObject<GPUICrowdPrefab> didn't on the non-nullable (or non-bitable) types. Basically, after calling:

     var prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(soldierPrefab, settings);


    the prefab exists, but is only basic transform data, it loses all of the unique scripts on gameobject, so later when I call:


    var ent = entityManager.Instantiate(prefab);


    the ent exists, but does not have any of the functionality i need or ability to get it's class or script type components and use them for anything. I think based on the ecs examples I need to do something like setup an icomponent data struct or some such that represents a Soldier prefab and all its info, but I'm not sure exactly. I guess what I'm asking is how to i spawn at runtime a bunch of entities based off a nontrivial prefab that needs to keep all its Unity and custom components and access to them?

    Edit: might I be better off just storing the transforms in like parallel NativeArrays and manipulating those while using the gameobjects normally otherwise and just setting their transforms to the native array transforms after all calcs are done, or something like that?
     
    Last edited: Nov 5, 2022
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    There's that. There's all TransformAccessArray and IJobParallelForTransform which may also be useful to you.