Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Coroutine Alternatives ! Benchmarks.

Discussion in 'Entity Component System' started by uani, Mar 6, 2022.

  1. uani

    uani

    Joined:
    Sep 6, 2013
    Posts:
    232
    I hope this is found by individuals who can build upon this.

    Project: https://github.com/u-an-i/Unity-Coroutine-Alternatives-Benchmark

    benchmarking baseline, Update, Coroutine, MEC Coroutine and 3 own implementations 2 of which use .NET Collections and 1 of which directly

    results:

    upload_2022-3-6_13-57-31.png

    winning code:

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. namespace u_i_2
    5. {
    6.     class batch
    7.     {
    8.         private const int size = 4218;
    9.  
    10.         public struct freed
    11.         {
    12.             public added number;
    13.             public int index;
    14.         }
    15.  
    16.         public class added
    17.         {
    18.             public added()
    19.             {
    20.                 content = new CoroutineReplacement2.crValues[size];
    21.                 free = new freed[size];
    22.             }
    23.  
    24.             public CoroutineReplacement2.crValues[] content;
    25.             public freed[] free;
    26.             public added next = null;
    27.         }
    28.  
    29.         public batch()
    30.         {
    31.             peakContent = new added();
    32.             baseFreed = peakContent;
    33.             peakFreed = baseFreed;
    34.             first = peakContent;
    35.             iteratorCurrent = first;
    36.         }
    37.  
    38.         private int iteratorIndex = -1;
    39.         private int peakPeak = 0;
    40.         private int baseFree = 0;
    41.         private int peakFree = -1;
    42.         private static CoroutineReplacement2.crValues nullValue = new CoroutineReplacement2.crValues()
    43.         {
    44.             enumerator = null
    45.         };
    46.         private added peakContent;
    47.         private added baseFreed;
    48.         private added peakFreed;
    49.         private added first;
    50.         private added iteratorCurrent;
    51.  
    52.         public CoroutineHandle Add(CoroutineReplacement2.crValues value)
    53.         {
    54.             added access;
    55.             int index;
    56.             if(peakFree >= 0)
    57.             {
    58.                 freed reference = baseFreed.free[baseFree];
    59.                 access = reference.number;
    60.                 index = reference.index;
    61.                 if (baseFree == peakFree && baseFreed == peakFreed)
    62.                 {
    63.                     baseFree = 0;
    64.                     peakFree = -1;
    65.                     baseFreed = first;
    66.                     peakFreed = baseFreed;
    67.                 }
    68.                 else if(++baseFree == size)
    69.                 {
    70.                     baseFree = 0;
    71.                     if (baseFreed == peakContent)
    72.                     {
    73.                         baseFreed = first;
    74.                     }
    75.                     else
    76.                     {
    77.                         baseFreed = baseFreed.next;
    78.                     }
    79.                 }
    80.             }
    81.             else
    82.             {
    83.                 access = peakContent;
    84.                 index = peakPeak;
    85.                 if(++peakPeak == size)
    86.                 {
    87.                     peakPeak = 0;
    88.                     peakContent.next = new added();
    89.                     peakContent = peakContent.next;
    90.                 }
    91.             }
    92.             access.content[index] = value;
    93.             return new CoroutineHandle()
    94.             {
    95.                 bin = this,
    96.                 access = access,
    97.                 index = index
    98.             };
    99.         }
    100.  
    101.         public ref CoroutineReplacement2.crValues GetNext()
    102.         {
    103.             if(++iteratorIndex == size)
    104.             {
    105.                 iteratorIndex = 0;
    106.                 iteratorCurrent = iteratorCurrent.next;
    107.             }
    108.             if(iteratorCurrent != null && (iteratorCurrent != peakContent || iteratorIndex < peakPeak))
    109.             {
    110.                 ref CoroutineReplacement2.crValues value = ref iteratorCurrent.content[iteratorIndex];
    111.                 return ref (value.enumerator != null ? ref value : ref GetNext());
    112.             }
    113.             else
    114.             {
    115.                 iteratorIndex = -1;
    116.                 iteratorCurrent = first;
    117.                 return ref nullValue;
    118.             }
    119.         }
    120.  
    121.         public void DeleteAtIterator()
    122.         {
    123.             iteratorCurrent.content[iteratorIndex].enumerator = null;
    124.             if (++peakFree == size)
    125.             {
    126.                 peakFree = 0;
    127.                 if (peakFreed == peakContent)
    128.                 {
    129.                     peakFreed = first;
    130.                 }
    131.                 else
    132.                 {
    133.                     peakFreed = peakFreed.next;
    134.                 }
    135.             }
    136.             peakFreed.free[peakFree] = new freed()
    137.             {
    138.                 number = iteratorCurrent,
    139.                 index = iteratorIndex
    140.             };
    141.         }
    142.  
    143.         public void DeleteWith(added access, int index)
    144.         {
    145.             access.content[index].enumerator = null;
    146.             if (++peakFree == size)
    147.             {
    148.                 peakFree = 0;
    149.                 if (peakFreed == peakContent)
    150.                 {
    151.                     peakFreed = first;
    152.                 }
    153.                 else
    154.                 {
    155.                     peakFreed = peakFreed.next;
    156.                 }
    157.             }
    158.             peakFreed.free[peakFree] = new freed()
    159.             {
    160.                 number = access,
    161.                 index = index
    162.             };
    163.         }
    164.  
    165.         public void DeleteAll()
    166.         {
    167.             peakContent = new added();
    168.             baseFreed = peakContent;
    169.             peakFreed = baseFreed;
    170.             first = peakContent;
    171.             iteratorCurrent = first;
    172.             iteratorIndex = -1;
    173.             peakPeak = 0;
    174.             baseFree = 0;
    175.             peakFree = -1;
    176.         }
    177.     };
    178.  
    179.     static class Timing
    180.     {
    181.         private static batch a = new batch();
    182.         private static batch[] bins = new batch[5]
    183.         {
    184.             new batch(),
    185.             new batch(),
    186.             new batch(),
    187.             new batch(),
    188.             new batch()
    189.         };
    190.  
    191.         public static CoroutineHandle RunCoroutine(IEnumerator<float> coroutine)
    192.         {
    193.             return a.Add(new CoroutineReplacement2.crValues()
    194.             {
    195.                 remainingTime = 0F,
    196.                 enumerator = coroutine
    197.             });
    198.         }
    199.  
    200.         public static CoroutineHandle RunCoroutine(IEnumerator<float> coroutine, int bin)
    201.         {
    202.             return bins[bin].Add(new CoroutineReplacement2.crValues()
    203.             {
    204.                 remainingTime = 0F,
    205.                 enumerator = coroutine
    206.             });
    207.         }
    208.  
    209.         public static float WaitForSeconds(float duration)
    210.         {
    211.             return duration;
    212.         }
    213.  
    214.         public static void KillCoroutines(CoroutineHandle handle)
    215.         {
    216.             handle.bin.DeleteWith(handle.access, handle.index);
    217.         }
    218.  
    219.         public static void KillCoroutines(int bin)
    220.         {
    221.             bins[bin].DeleteAll();
    222.         }
    223.  
    224.         public static transfer getCoroutines()
    225.         {
    226.             return new transfer()
    227.             {
    228.                 a = a,
    229.                 bins = bins
    230.             };
    231.         }
    232.  
    233.         public struct transfer
    234.         {
    235.             public batch a;
    236.             public batch[] bins;
    237.         };
    238.     }
    239.  
    240.     class CoroutineReplacement2 : MonoBehaviour
    241.     {
    242.         public struct crValues
    243.         {
    244.             public float remainingTime;
    245.             public IEnumerator<float> enumerator;
    246.         };
    247.  
    248.         private batch a;
    249.         private batch[] bins;
    250.  
    251.         // Start is called before the first frame update
    252.         void Start()
    253.         {
    254.             Timing.transfer coroutines = Timing.getCoroutines();
    255.             a = coroutines.a;
    256.             bins = coroutines.bins;
    257.         }
    258.  
    259.         // Update is called once per frame
    260.         void Update()
    261.         {
    262.             ref crValues value = ref a.GetNext();
    263.             while (value.enumerator != null)
    264.             {
    265.                 value.remainingTime -= Time.deltaTime;
    266.                 if (value.remainingTime <= 0F)
    267.                 {
    268.                     if (value.enumerator.MoveNext())
    269.                     {
    270.                         value.remainingTime = value.enumerator.Current;
    271.                     }
    272.                     else
    273.                     {
    274.                         a.DeleteAtIterator();
    275.                     }
    276.                 }
    277.                 value = ref a.GetNext();
    278.             }
    279.             int i = 0;
    280.             do
    281.             {
    282.                 batch b = bins[i];
    283.                 ref crValues val = ref b.GetNext();
    284.                 while (val.enumerator != null)
    285.                 {
    286.                     val.remainingTime -= Time.deltaTime;
    287.                     if (val.remainingTime <= 0F)
    288.                     {
    289.                         if (val.enumerator.MoveNext())
    290.                         {
    291.                             val.remainingTime = val.enumerator.Current;
    292.                         }
    293.                         else
    294.                         {
    295.                             b.DeleteAtIterator();
    296.                         }
    297.                     }
    298.                     val = ref b.GetNext();
    299.                 }
    300.             }
    301.             while (++i < 5);
    302.         }
    303.     }
    304.  
    305.     struct CoroutineHandle
    306.     {
    307.         public batch bin;
    308.         public batch.added access;
    309.         public int index;
    310.     }
    311. }

    Video:

     
    apkdev likes this.
  2. uani

    uani

    Joined:
    Sep 6, 2013
    Posts:
    232
    I'm aware this is not DOTS but performance-related for users who use the classic way.
     
  3. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    777
    Does it make sense to benchmark Coroutine? Basically, every yield waits for the next frame. a loop with 100 yields always takes 100 frames, regardless of the work. And without a yield, a coroutine would not make much sense, since it would then be processed within the same frame anyway +overhead.
     
  4. uani

    uani

    Joined:
    Sep 6, 2013
    Posts:
    232
    Overhead results in longer frametime (no vsync or …). But number of frames is number of yields returning „0“ plus those during waiting for seconds.
     
    apkdev likes this.