Search Unity

Question Job System Performance on iOS

Discussion in 'C# Job System' started by WYD7, Aug 29, 2022.

  1. WYD7

    WYD7

    Joined:
    Sep 1, 2021
    Posts:
    2
    I am trying to find the optimal number of jobs to use on iOS. I created an array and a various number of IJob structs to update different parts of the array. I found that regardless of the number of jobs I created, the time to complete updating the array using the job system is always longer than without using the job system (i.e. using just the main thread). Is this expected? Basically, does IJob lead to worse perfomance on iOS?

    I tested different array lengths from 100 to 10000 and updating is just incrementing every array element. I tested 1 to 100 jobs.
     
  2. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    5,048
    Packing and distributing the jobs will take up CPU time as well. If the code doesn't do that much this can take up more than the performance benefit of multithreading.
    Maybe ios does do something weird, but then you should see some logs pop up.

    You could try with more intense jobs to see if it helps.
     
  3. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,892
    You may want to use IJobParallelFor instead. That will be more efficient when it comes to parallel processing. If you really need a job for indices 0-999 and 1000-1999 and so on you can make use of NativeSlice to essentially "split" the array into chunks without actually making copies of the array (I believe).


    Keep in mind that it matters a lot when you call Complete(). For example:
    Code (CSharp):
    1. job.Schedule().Complete();
    If job is an IJob this is essentially blocking the main thread waiting on the background IJob thread to finish. If however you Schedule one or more jobs in Update and call Complete() in LateUpdate this can essentially mean "free processing" during the time between Update and LateUpdate (or even between two frames' Update/LateUpdate depending on whether you really need the results right away).

    Lastly, be sure that under the Jobs menu Burst and Job threads are enabled and under Project Settings => Burst AOT Settings check the platform-specific settings. For measuring you should set OptimizeFor to Performance, this overrides the same settings specified via BurstCompile attribute.
     
    DevDunk likes this.
  4. WYD7

    WYD7

    Joined:
    Sep 1, 2021
    Posts:
    2
    Sorry for not posting my code earlier, but this is what I have. I'm using the Unity Perfomance Testing Extension to measure time. I'm using IJob instread of IJobParallelFor because I can precisely control the number of jobs allocated with IJob but with IJobParallelFor Unity actually schedules "an appropriate number of jobs" according to the docs.

    I also tried using [BurstCompile], which gave a performance boost.

    I use the command line to run the performance tests and the results are .xml files which I use a python script to parse.

    Code (CSharp):
    1. internal class BenchUnityJobSystem
    2. {
    3.     private const int WARM_UP = 10;
    4.     private const int ITERATIONS = 10;
    5.     private const int MEASUREMENTS = 100;
    6.     private const int MAX_JOB_COUNT = 100;
    7.     private static int[] s_job_counts = InitJobCounts();
    8.     private static float[] s_result_array;
    9.     private static List<BenchJob> s_jobs;
    10.     private static NativeArray<JobHandle> s_jobHandles;
    11.  
    12.     private static int[] InitJobCounts()
    13.     {
    14.         var job_counts = new int[MAX_JOB_COUNT + 1];
    15.         for (var i = 0; i <= MAX_JOB_COUNT; i++)
    16.         {
    17.             job_counts[i] = i;
    18.         }
    19.         return job_counts;
    20.     }
    21.  
    22.     [Test, Performance]
    23.     public static void TestArray100([ValueSource(nameof(s_job_counts))] int jobCount)
    24.     {
    25.         RunExperiments(jobCount, 100);
    26.     }
    27.  
    28.     [Test, Performance]
    29.     public static void TestArray500([ValueSource(nameof(s_job_counts))] int jobCount)
    30.     {
    31.         RunExperiments(jobCount, 500);
    32.     }
    33.  
    34.     [Test, Performance]
    35.     public static void TestArray1000([ValueSource(nameof(s_job_counts))] int jobCount)
    36.     {
    37.         RunExperiments(jobCount, 1000);
    38.     }
    39.  
    40.     [Test, Performance]
    41.     public static void TestArray5000([ValueSource(nameof(s_job_counts))] int jobCount)
    42.     {
    43.         RunExperiments(jobCount, 5000);
    44.     }
    45.  
    46.     [Test, Performance]
    47.     public static void TestArray10000([ValueSource(nameof(s_job_counts))] int jobCount)
    48.     {
    49.         RunExperiments(jobCount, 10000);
    50.     }
    51.  
    52.     [Test, Performance]
    53.     public static void TestArray50000([ValueSource(nameof(s_job_counts))] int jobCount)
    54.     {
    55.         RunExperiments(jobCount, 50000);
    56.     }
    57.  
    58.     [Test, Performance]
    59.     public static void TestArray100000([ValueSource(nameof(s_job_counts))] int jobCount)
    60.     {
    61.         RunExperiments(jobCount, 100000);
    62.     }
    63.  
    64.     private static void InitializeExperiments(
    65.         int jobCount,
    66.         int arraySize,
    67.         NativeArray<float> result
    68.     )
    69.     {
    70.         s_result_array = new float[arraySize];
    71.         s_jobs = new List<BenchJob>();
    72.         if (jobCount > 0)
    73.         {
    74.             var startIndex = 0;
    75.             var jobSize = arraySize / jobCount;
    76.             var bigJobSize = jobSize + 1; // big job has 1 more array element than average job, to evenly distribute array
    77.             var bigJobCount = arraySize % jobCount;
    78.             for (var i = 0; i < jobCount; i++)
    79.             {
    80.                 var job = new BenchJob()
    81.                 {
    82.                     slice = new NativeSlice<float>(
    83.                         result,
    84.                         startIndex,
    85.                         i < bigJobCount ? bigJobSize : jobSize
    86.                     )
    87.                 };
    88.                 startIndex += i < bigJobCount ? bigJobSize : jobSize;
    89.                 s_jobs.Add(job);
    90.             }
    91.             Debug.Assert(startIndex == arraySize);
    92.         }
    93.     }
    94.  
    95.     private static void RunExperiments(int jobCount, int arraySize)
    96.     {
    97.         var result = new NativeArray<float>(arraySize, Allocator.TempJob);
    98.         s_jobHandles = new NativeArray<JobHandle>(jobCount, Allocator.TempJob);
    99.         InitializeExperiments(jobCount, arraySize, result);
    100.         Measure
    101.             .Method(() =>
    102.             {
    103.                 if (jobCount != 0)
    104.                 {
    105.                     UseJobSystem();
    106.                 }
    107.                 else
    108.                 {
    109.                     NoJobSystemNativeArray(result);
    110.                 }
    111.             })
    112.             .WarmupCount(WARM_UP)
    113.             .IterationsPerMeasurement(ITERATIONS)
    114.             .MeasurementCount(MEASUREMENTS)
    115.             .Run();
    116.         result.Dispose();
    117.         s_jobHandles.Dispose();
    118.     }
    119.  
    120.     public static void UseJobSystem()
    121.     {
    122.         for (var i = 0; i < s_jobs.Count; i++)
    123.         {
    124.             var job = s_jobs[i];
    125.             s_jobHandles[i] = job.Schedule();
    126.         }
    127.         JobHandle.CompleteAll(s_jobHandles);
    128.     }
    129.  
    130.     private static void NoJobSystemNativeArray(NativeArray<float> result)
    131.     {
    132.         for (var i = 0; i < result.Length; i++)
    133.         {
    134.             var temp = result[i];
    135.             temp += 1;
    136.             result[i] = temp;
    137.         }
    138.     }
    139.  
    140.     // [BurstCompile]
    141.     public struct BenchJob : IJob
    142.     {
    143.         [NativeDisableContainerSafetyRestriction]
    144.         public NativeSlice<float> slice;
    145.  
    146.         public void Execute()
    147.         {
    148.             for (var i = 0; i < slice.Length; i++)
    149.             {
    150.                 var temp = slice[i];
    151.                 temp += 1;
    152.                 slice[i] = temp;
    153.             }
    154.         }
    155.     }
    156. }
     
  5. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,892
    ALWAYS use BurstCompile! You'll waste a ton of performance not doing so. In fact, it's not uncommon to see a far greater gain in performance by using Burst compared to parallelizing a job without Burst!

    For example, I do some parallel processing in the editor on meshes with Jobs and this finishes in 20 ms with Burst enabled:
    upload_2022-8-30_15-18-6.png

    Now I go to Jobs => Burst => Enable Compilation [unchecked]:
    upload_2022-8-30_15-18-41.png

    Posting screenshots in case you won't believe me. :D

    More so, when I would run this process the way I had it before I ported it to Jobs and Burst, this would have taken ... roughly 5 seconds minimum.

    From the kind of tests you perform I strongly recommend to stop wasting your time on profiling artificial stuff like that. Write actual game code, profile that, then optimize if necessary! You'll eventually get a grip on what is better in which scenario but unless you're actually computing something meaningful in these jobs you will not gain any insight.

    Any performance difference you'll see in tests like these will very likely not matter or even give a false impression of whats faster when you get to writing actual game code.
     
  6. elliotc-unity

    elliotc-unity

    Unity Technologies

    Joined:
    Nov 5, 2015
    Posts:
    230
    The editor is not a fair comparison really; the editor runs mono, where ios builds are on il2cpp. Also, the editor has collections checks (which burst can strip out if you have burst safety checks off), where ios builds don't. Burst still helps in builds, sometimes by a lot, but it's rarely by the same insane factor that it is in the editor.

    That said, the original experiment as written is not necessarily a fair comparison either. The job system version allocates a large managed array for every experiment (and then seemingly not use it?), which will take time by itself and will also generate GC pressure, which could easily spike during the test execution.

    Also, the jobs access the data via nativeslices, whereas the main thread accesses the nativearray directly. I don't know if this explains the whole difference; there is also scheduling overhead, which we have worked hard to reduce in 22.2. You can also burst the schedule site in 22.2, which should help with that overhead. Said overhead will be more apparent the less work each job does, so if you see the jobs get relatively faster as the workload gets bigger, that could be what's going on.
     
    Elapotp and DevDunk like this.