Search Unity

Calculating average data across multiple chunks

Discussion in 'Entity Component System' started by JarrydFP, Apr 1, 2019.

  1. JarrydFP

    JarrydFP

    Joined:
    Oct 6, 2017
    Posts:
    3
    I'm running into an issue calculating average values across multiple chunks. The data appears correct when the number of entities is contained within a chunk, but once that number of entities spans over multiple chunks, the average calculation becomes inconsistent and changes from frame to frame. I've managed to repro it into a single script:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6. using UnityEngine;
    7.  
    8. public struct RandomData : IComponentData
    9. {
    10.     public float RandomFloat;
    11. }
    12.  
    13. [AlwaysUpdateSystem]
    14. public class TestSystem : JobComponentSystem
    15. {
    16.     private NativeArray<float> averagesArray;//0 = total, 1 = count, 2 = average calculated
    17.  
    18.     private ComponentGroup randomGroup;
    19.    
    20.     protected override void OnCreateManager()
    21.     {
    22.         base.OnCreateManager();
    23.        
    24.         averagesArray = new NativeArray<float>(3, Allocator.Persistent);
    25.         randomGroup = GetComponentGroup(ComponentType.Create<RandomData>());
    26.     }
    27.  
    28.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    29.     {
    30.         //process results from last frames averages
    31.         Debug.Log($"Total: {averagesArray[0]}, count: {averagesArray[1]}, avg: {averagesArray[2]}");
    32.  
    33.         averagesArray[0] = 0f;
    34.         averagesArray[1] = 0f;
    35.         averagesArray[2] = 0f;
    36.        
    37.         if (Input.GetKeyUp(KeyCode.Space))
    38.         {
    39.             Debug.Log("Create");
    40.             //create 100 things
    41.             for (int i = 0; i < 500; i++)
    42.             {
    43.                 var newEntity = EntityManager.CreateEntity();
    44.                 EntityManager.AddComponentData(newEntity, new RandomData()
    45.                 {
    46.                     RandomFloat = Random.Range(0, 100f),
    47.                 });
    48.             }
    49.         }
    50.  
    51.         inputDeps = new UpdateGlobalAverages()
    52.         {
    53.             RandomData = GetArchetypeChunkComponentType<RandomData>(),
    54.             AveragesArray = averagesArray,
    55.             FrameCount = Time.frameCount
    56.         }.Schedule(randomGroup);
    57.        
    58.         return inputDeps;
    59.     }
    60.    
    61.     private struct UpdateGlobalAverages : IJobChunk
    62.     {
    63.         [ReadOnly] public ArchetypeChunkComponentType<RandomData> RandomData;
    64.  
    65.         public NativeArray<float> AveragesArray;
    66.  
    67.  
    68.         public int FrameCount;
    69.        
    70.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    71.         {
    72.             var dataArray = chunk.GetNativeArray(RandomData);
    73.            
    74.             var dataCount = chunk.Count;
    75.  
    76.             for (int i = 0; i < dataCount; i++)
    77.             {
    78.                 AveragesArray[0] = AveragesArray[0] + dataArray[i].RandomFloat;
    79.             }
    80.  
    81.             AveragesArray[1] = AveragesArray[1] + dataCount;
    82.             AveragesArray[2] = AveragesArray[0] / AveragesArray[1];
    83.            
    84.             Debug.Log($"{FrameCount}:{dataCount}");
    85.         }
    86.     }
    87. }
    88.  
    Once you hit space more than twice and create over 1000 entities, the average calculation starts changing from frame to frame, despite the data staying static: https://files.facepunch.com/jarryd/2019/04/01/Unity_2019-04-01_21-05-28.jpg

    Have I misunderstood chunk processing?
     
  2. ilih

    ilih

    Joined:
    Aug 6, 2013
    Posts:
    1,412
    IJobChunk is multithreaded and runs for each chunk. Each chunk can store a fixed amount of entities (depend on struct size).
    Where you create new entities more than chunk limit then will be created a new chunk so that way you can see two lines from "Debug.Log($"{FrameCount}:{dataCount}");".
    And now you have two IJobChunk running at the same time and reading and writing the same value without synchronization and because of it, some values can be lost.

    Probably replacing NativeArray with two NativeList (one for sum per chunk and another for count per chunk) and then calculating the average in the separate IJob will help.
     
  3. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    Yeah what ilih said I think is probably the problem.
    Also you're read/write'ing to AveragesArray constantly so very likely to hit contention.

    I don't know if this still valid anymore as I no longer use it but I solved similar problem in past with [NativeSetThreadIndex].
    You can try this. This just pseudo, I haven't tested it.
    Code (CSharp):
    1. struct AverageData {
    2.     public int count;
    3.     public float total;
    4. }
    5.  
    6. onCreateManager() {
    7.     int threadCount = Unity.Jobs.LowLevel.Unsafe.JobsUtility.MaxJobThreadCount;
    8.     averagesArray = new NativeArray<AverageData>(threadCount, Allocator.Persistent);
    9. }
    10.  
    11. private struct UpdateGlobalAverages : IJobChunk
    12. {
    13.     [NativeSetThreadIndex] private int threadIndex;
    14.      
    15.     public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    16.     {
    17.         ...
    18.         float total = 0;
    19.         for (int i = 0; i < dataCount; i++)    {
    20.             total += dataArray[i].RandomFloat;
    21.         }
    22.         AverageData ad = AveragesArray[threadIndex];
    23.         ad.total += total;
    24.         ad.count += dataCount;
    25.         AveragesArray[threadIndex] = ad;
    26.     }
    27. }
    Then you can compute your average from AveragesArray in OnUpdate() and zero out the array again.