Search Unity

Resolved Run Job on array of objects

Discussion in 'Entity Component System' started by EternalAmbiguity, Apr 12, 2021.

  1. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I'm making a game that has a backend simulation that has a 2D array of objects, something like this:

    Code (csharp):
    1.  
    2. class Parent
    3. {
    4.     public Child[,] grid;
    5. }
    6.  
    7. class Child
    8. {
    9.     public float value;
    10. }
    Right now my grid size is about 40,000 total but I plan for it to be much larger.

    I need to process the float value in the child class (average the value between it and its 4 cardinal neighbors).

    Right now I'm just doing this in regular C# code which obviously takes a long time. I'm employing some optimizations (maintaining a list of locations to calculate and removing a location if its value doesn't change) but ultimately I'd like to use Jobs to avoid regular C# threading. Is that possible?

    Converting these objects into structs or entities isn't an option: I just want a Job that can act on an array of objects. Is that possible, and if so are there any examples about?
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    No it is not. You will have to change up your data structure. If you can provide more context and constraints for the problem at hand (what else can we assume about the problem), we may be able to provide you a satisfactory solution.
     
  3. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay thanks. This is pretty much everything. These are temperatures so I'm propagating changes spatially. The values are used for another object that sits at the same location. The values probably don't HAVE to be tied to objects, they could be a dictionary or something with (int,int) as the key, but they need to be C# data structures that are part of an already-existing static class.
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    That sounds like you could abstract the actual data location behind a C# property. And you can lazy-initialize a NativeContainer easy enough. Your challenge is going to be teardown. Do you have a mechanism for working with IDisposable's inside your static class?

    Otherwise your best option would be to use a C# array and then there are mechanisms to convert that into a temporary NativeArray which you can use in a job. The benefit of that approach is that the copy between the C# array and NativeArray uses native code, so it is significantly faster. And you may even find some threads describing how to interpret a C# array as a NativeArray temporarily, avoiding the memcpy altogether.
     
    EternalAmbiguity and Antypodish like this.
  5. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,780
    Using go is probably worse thing, if you concern about performance.
    Why even using go, to store temperature value? That is massive i efficency and waste.
    You may indeed rethink your design, as other suggested already.
     
    EternalAmbiguity likes this.
  6. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    By "go" do you mean GameObject? Not a GameObject, just a C# class so a C# "object" in the sense than a struct is not an object. It's a class that contains a grid/2D array of C# classes (so "objects") where the temperature is a variable within that subclass. Is my nomenclature wrong?

    Okay, at start I can just create an array of floats then. I've heard that 1D arrays are significantly faster than 2D, but in this case I'll need the spatial information so I'll be converting between 2D and "flattened" indices multiple times. Oh well.

    I'll take a look around for stuff modifying a simple array of floats with a Job (while accessing that array or a copy within the job).
     
    Last edited: Apr 12, 2021
  7. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,780
    Yes. And fair enough. I hastily assumed GO, where you stated Object. So cookie for you :)
    Nah, you were right.

    But yes, if you can flatten an array, that will be much better.
     
    EternalAmbiguity likes this.
  8. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay, I was able to get it working. Seems like jobs have to be scheduled and completed on the main thread, so I'm doing the following:

    Code (csharp):
    1. class CA
    2. {
    3.  
    4.     public float[] TempArray { get; private set; }
    5.     static JobHandle jobHandle;
    6.     static Unity.Collections.NativeArray<float> input;
    7.     static Unity.Collections.NativeArray<float> output;
    8.  
    9.     public ulong OneIteration((int,int)[] moving)
    10.     {
    11.         var now = DateTime.Now;
    12.         output = new Unity.Collections.NativeArray<float>(TempArray.Length, Unity.Collections.Allocator.Persistent);
    13.         StaticClass.mainThreadContext.Post(_ =>
    14.         {
    15.             input = new Unity.Collections.NativeArray<float>(TempArray, Unity.Collections.Allocator.Persistent);
    16.             output = new Unity.Collections.NativeArray<float>(TempArray.Length, Unity.Collections.Allocator.Persistent);
    17.             var job = new TempJob()
    18.             {
    19.                 input = input,
    20.                 output = output,
    21.                 height = gridHeight,
    22.                 width = gridWidth
    23.             };
    24.             jobHandle = job.Schedule(TempArray.Length, 16);
    25.         }, null);
    26. // stuff in between
    27.         var _1 = (DateTime.Now - now);
    28.         StaticClass.mainThreadContext.Post(_ =>
    29.         {
    30.             jobHandle.Complete();
    31.         }, null);
    32.  
    33.         var _2 = (DateTime.Now - now);
    34.         TempArray = output.ToArray();
    35.         StaticClass.mainThreadContext.Post(_ =>
    36.         {
    37.             if (input.IsCreated)
    38.             {
    39.                 input.Dispose();
    40.             }
    41.             output.Dispose();
    42.         }, null);
    43. // other stuff
    44.     }
    45.  
    46.     struct TempJob : Unity.Jobs.IJobParallelFor
    47.     {
    48.         // Jobs declare all data that will be accessed in the job
    49.         // By declaring it as read only, multiple jobs are allowed to access the data in parallel
    50.         [Unity.Collections.ReadOnly]
    51.         public Unity.Collections.NativeArray<float> input;
    52.         [Unity.Collections.ReadOnly]
    53.         public int height;
    54.         [Unity.Collections.ReadOnly]
    55.         public int width;
    56.  
    57.         // By default containers are assumed to be read & write
    58.         public Unity.Collections.NativeArray<float> output;
    59.  
    60.         // The code actually running on the job
    61.         public void Execute(int i)
    62.         {
    63.             int x = i % width;
    64.             int y = i / width;
    65.             int total = 1;
    66.             float cur = input[i];
    67.             float p1 = GetValue(x + 1, y);
    68.             if(p1 > 0)
    69.             {
    70.                 cur += p1;
    71.                 total++;
    72.             }
    73.             float p2 = GetValue(x - 1, y);
    74.             if (p2 > 0)
    75.             {
    76.                 cur += p2;
    77.                 total++;
    78.             }
    79.             float p3 = GetValue(x, y+1);
    80.             if (p3 > 0)
    81.             {
    82.                 cur += p3;
    83.                 total++;
    84.             }
    85.             float p4 = GetValue(x, y - 1);
    86.             if (p4 > 0)
    87.             {
    88.                 cur += p4;
    89.                 total++;
    90.             }
    91.             output[i] = cur / total;
    92.         }
    93.  
    94.         public float GetValue(int x, int y)
    95.         {
    96.             if (x >= 0 && x < width && y >= 0 && y < height)
    97.             {
    98.                 int index = y + (x * height);
    99.                 //if(index > input.Length)
    100.                 //{
    101.                 //    ;
    102.                 //}
    103.                 return input[index];
    104.             }
    105.             else
    106.             {
    107.                 return -1;
    108.             }
    109.         }
    110.     }
    111. }
    I'm not 100% sure what the batch number should be, but I can play around with it. Should I be using [BurstCompile] anywhere?
     
  9. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    Yes. On struct TempJob : IJobFor

    It would also be useful to share performance differences between the old version and the new version for future readers.
     
    EternalAmbiguity likes this.
  10. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay, I spoke too soon. Had to change a couple things, but it's working now. Current code is below.

    Code (csharp):
    1.  
    2.     static float[] TempArray { get; set; }
    3.     static JobHandle jobHandle;
    4.     static Unity.Collections.NativeArray<float> input;
    5.     static Unity.Collections.NativeArray<float> output;
    6.     static bool modified = false;
    7.  
    8. // inside iteration method
    9.         var _1 = (DateTime.Now - now);
    10.         modified = false;
    11.         output = new Unity.Collections.NativeArray<float>(TempArray.Length, Unity.Collections.Allocator.Persistent);
    12.         StaticClass.mainThreadContext.Post(_ =>
    13.         {
    14.             input = new Unity.Collections.NativeArray<float>(TempArray, Unity.Collections.Allocator.Persistent);
    15.             output = new Unity.Collections.NativeArray<float>(TempArray.Length, Unity.Collections.Allocator.Persistent);
    16.             var job = new TempJob()
    17.             {
    18.                 input = input,
    19.                 output = output,
    20.                 height = gridHeight,
    21.                 width = gridWidth
    22.             };
    23.             jobHandle = job.Schedule(TempArray.Length, 16);
    24.         }, null);
    25.         StaticClass.mainThreadContext.Post(_ =>
    26.         {
    27.             jobHandle.Complete();
    28.             if(!modified)
    29.             {
    30.                 TempArray = output.ToArray();
    31.             }
    32.             else
    33.             {
    34.                 ;
    35.             }
    36.             if (input.IsCreated)
    37.             {
    38.                 input.Dispose();
    39.             }
    40.             output.Dispose();
    41.         }, null);
    42.         var _2 = (DateTime.Now - now);
    43.         times.Add((float)(_2.TotalMilliseconds - _1.TotalMilliseconds));
    44.         if(times.Count == 1000)
    45.         {
    46.             var avg = times.Average();
    47.             times.Clear();
    48.             UnityEngine.Debug.Log("prev 100: " + avg);
    49.         }
    50. // outside iteration method
    51.  
    52.     [BurstCompile]
    53.     struct TempJob : Unity.Jobs.IJobParallelFor
    54.     {
    55.         // Jobs declare all data that will be accessed in the job
    56.         // By declaring it as read only, multiple jobs are allowed to access the data in parallel
    57.         [Unity.Collections.ReadOnly]
    58.         public Unity.Collections.NativeArray<float> input;
    59.         [Unity.Collections.ReadOnly]
    60.         public int height;
    61.         [Unity.Collections.ReadOnly]
    62.         public int width;
    63.  
    64.         // By default containers are assumed to be read & write
    65.         public Unity.Collections.NativeArray<float> output;
    66.  
    67.         // The code actually running on the job
    68.         public void Execute(int i)
    69.         {
    70.             if(input[i] < 0)
    71.             {
    72.                 output[i] = -1;
    73.             }
    74.             else
    75.             {
    76.                 int x = i % width;
    77.                 int y = i / width;
    78.                 int total = 1;
    79.                 float cur = input[i];
    80.                 float p1 = GetValue(x + 1, y);
    81.                 if (p1 > 0)
    82.                 {
    83.                     cur += p1;
    84.                     total++;
    85.                 }
    86.                 float p2 = GetValue(x - 1, y);
    87.                 if (p2 > 0)
    88.                 {
    89.                     cur += p2;
    90.                     total++;
    91.                 }
    92.                 float p3 = GetValue(x, y + 1);
    93.                 if (p3 > 0)
    94.                 {
    95.                     cur += p3;
    96.                     total++;
    97.                 }
    98.                 float p4 = GetValue(x, y - 1);
    99.                 if (p4 > 0)
    100.                 {
    101.                     cur += p4;
    102.                     total++;
    103.                 }
    104.                 output[i] = cur / total;
    105.             }
    106.         }
    107.  
    108.         public float GetValue(int x, int y)
    109.         {
    110.             if (x >= 0 && x < width && y >= 0 && y < height)
    111.             {
    112.                 int index = x + (y * width);
    113.                 //if(index > input.Length)
    114.                 //{
    115.                 //    ;
    116.                 //}
    117.                 return input[index];
    118.             }
    119.             else
    120.             {
    121.                 return -1;
    122.             }
    123.         }
    124.     }
    Testing: I ran the TempArray in a Parallel.For loop, so no job (basically just using Execute), in between _1 and _2 for 1000 iterations to arrive at 0.8 total milliseconds for the operation.

    I ran the code above, looping for 1000 iterations with batches of 8, 16, and 32. My times were 0.068, 0.055, and 0.068, respectively. Do note that this is with the job scheduling and "complete" being immediately after one another. If I shift the scheduling back to the start of the "OneIteration" method (so the only thing in between _1 and _2 is waiting for completion), I get an average time of 0.002.
     
    bb8_1 likes this.
  11. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    451
    You can improve performance even more by avoiding branching. You can look into my take on implementing game of life and how it is possible to avoid branching.
    https://github.com/micz84/GameOfLife
     
    Last edited: Apr 14, 2021
  12. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Hate to do this...but I found out it wasn't working correctly. Apparently SyncronizationContext.Post only starts a method on whatever thread and continues, rather than waiting. I need to wait for the job to complete. Just changed my code to the following:

    Code (csharp):
    1.  
    2. var _0 = (DateTime.Now - now);
    3.         StaticClass.mainThreadContext.Send(_ =>
    4.         {
    5.             jobHandle.Complete();
    6.             if (StaticClass.modifiedTemp == false)
    7.             {
    8.                 TempArray = output.ToArray();
    9.             }
    10.             else
    11.             {
    12.                 ;
    13.             }
    14.             if (input.IsCreated)
    15.             {
    16.                 input.Dispose();
    17.             }
    18.             output.Dispose();
    19.         }, null);
    20.         var _1 = (DateTime.Now - now);
    21.         Parallel.For(0, TempArray.Length, (i) =>
    22.         {
    23.             Execute(i);
    24.         });
    25.         if(!StaticClass.modifiedTemp)
    26.         {
    27.             Array.Copy(writeArray, TempArray, TempArray.Length);
    28.         }
    29.         var _2 = (DateTime.Now - now);
    30.         times.Add((_1 - _0).TotalMilliseconds);
    31.         times2.Add((_2 - _1).TotalMilliseconds);
    32.         if (times.Count == 1000)
    33.         {
    34.             var avg = times.Average();
    35.             var avg2 = times2.Average();
    36.             times.Clear();
    37.             times2.Clear();
    38.             UnityEngine.Debug.Log("average: " + avg + " & " + avg2);
    39.         }
    briefly, this forces a wait for the method on the main thread to complete before continuing, and compares that to a Parallel.for. (the Parallel.For is doing basically the same thing as is in the job).

    Got it running now...
    tempFlow.png
    Looks like Parallel.For is faster. Even if I limit it to 2 threads, it's taking about 3.5 seconds vs. 14.5 for the job.
     
    Last edited: Apr 17, 2021
    bb8_1 likes this.