Coroutine Alternatives ! Benchmarks.

I hope this is found by individuals who can build upon this.

Project: GitHub - u-an-i/Unity-Coroutine-Alternatives-Benchmark: benchmark of ways to "animate" in Unity

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

results:

winning code:

using System.Collections.Generic;
using UnityEngine;

namespace u_i_2
{
    class batch
    {
        private const int size = 4218;

        public struct freed
        {
            public added number;
            public int index;
        }

        public class added
        {
            public added()
            {
                content = new CoroutineReplacement2.crValues[size];
                free = new freed[size];
            }

            public CoroutineReplacement2.crValues[] content;
            public freed[] free;
            public added next = null;
        }

        public batch()
        {
            peakContent = new added();
            baseFreed = peakContent;
            peakFreed = baseFreed;
            first = peakContent;
            iteratorCurrent = first;
        }

        private int iteratorIndex = -1;
        private int peakPeak = 0;
        private int baseFree = 0;
        private int peakFree = -1;
        private static CoroutineReplacement2.crValues nullValue = new CoroutineReplacement2.crValues()
        {
            enumerator = null
        };
        private added peakContent;
        private added baseFreed;
        private added peakFreed;
        private added first;
        private added iteratorCurrent;

        public CoroutineHandle Add(CoroutineReplacement2.crValues value)
        {
            added access;
            int index;
            if(peakFree >= 0)
            {
                freed reference = baseFreed.free[baseFree];
                access = reference.number;
                index = reference.index;
                if (baseFree == peakFree && baseFreed == peakFreed)
                {
                    baseFree = 0;
                    peakFree = -1;
                    baseFreed = first;
                    peakFreed = baseFreed;
                }
                else if(++baseFree == size)
                {
                    baseFree = 0;
                    if (baseFreed == peakContent)
                    {
                        baseFreed = first;
                    }
                    else
                    {
                        baseFreed = baseFreed.next;
                    }
                }
            }
            else
            {
                access = peakContent;
                index = peakPeak;
                if(++peakPeak == size)
                {
                    peakPeak = 0;
                    peakContent.next = new added();
                    peakContent = peakContent.next;
                }
            }
            access.content[index] = value;
            return new CoroutineHandle()
            {
                bin = this,
                access = access,
                index = index
            };
        }

        public ref CoroutineReplacement2.crValues GetNext()
        {
            if(++iteratorIndex == size)
            {
                iteratorIndex = 0;
                iteratorCurrent = iteratorCurrent.next;
            }
            if(iteratorCurrent != null && (iteratorCurrent != peakContent || iteratorIndex < peakPeak))
            {
                ref CoroutineReplacement2.crValues value = ref iteratorCurrent.content[iteratorIndex];
                return ref (value.enumerator != null ? ref value : ref GetNext());
            }
            else
            {
                iteratorIndex = -1;
                iteratorCurrent = first;
                return ref nullValue;
            }
        }

        public void DeleteAtIterator()
        {
            iteratorCurrent.content[iteratorIndex].enumerator = null;
            if (++peakFree == size)
            {
                peakFree = 0;
                if (peakFreed == peakContent)
                {
                    peakFreed = first;
                }
                else
                {
                    peakFreed = peakFreed.next;
                }
            }
            peakFreed.free[peakFree] = new freed()
            {
                number = iteratorCurrent,
                index = iteratorIndex
            };
        }

        public void DeleteWith(added access, int index)
        {
            access.content[index].enumerator = null;
            if (++peakFree == size)
            {
                peakFree = 0;
                if (peakFreed == peakContent)
                {
                    peakFreed = first;
                }
                else
                {
                    peakFreed = peakFreed.next;
                }
            }
            peakFreed.free[peakFree] = new freed()
            {
                number = access,
                index = index
            };
        }

        public void DeleteAll()
        {
            peakContent = new added();
            baseFreed = peakContent;
            peakFreed = baseFreed;
            first = peakContent;
            iteratorCurrent = first;
            iteratorIndex = -1;
            peakPeak = 0;
            baseFree = 0;
            peakFree = -1;
        }
    };

    static class Timing
    {
        private static batch a = new batch();
        private static batch[] bins = new batch[5]
        {
            new batch(),
            new batch(),
            new batch(),
            new batch(),
            new batch()
        };

        public static CoroutineHandle RunCoroutine(IEnumerator<float> coroutine)
        {
            return a.Add(new CoroutineReplacement2.crValues()
            {
                remainingTime = 0F,
                enumerator = coroutine
            });
        }

        public static CoroutineHandle RunCoroutine(IEnumerator<float> coroutine, int bin)
        {
            return bins[bin].Add(new CoroutineReplacement2.crValues()
            {
                remainingTime = 0F,
                enumerator = coroutine
            });
        }

        public static float WaitForSeconds(float duration)
        {
            return duration;
        }

        public static void KillCoroutines(CoroutineHandle handle)
        {
            handle.bin.DeleteWith(handle.access, handle.index);
        }

        public static void KillCoroutines(int bin)
        {
            bins[bin].DeleteAll();
        }

        public static transfer getCoroutines()
        {
            return new transfer()
            {
                a = a,
                bins = bins
            };
        }

        public struct transfer
        {
            public batch a;
            public batch[] bins;
        };
    }

    class CoroutineReplacement2 : MonoBehaviour
    {
        public struct crValues
        {
            public float remainingTime;
            public IEnumerator<float> enumerator;
        };

        private batch a;
        private batch[] bins;

        // Start is called before the first frame update
        void Start()
        {
            Timing.transfer coroutines = Timing.getCoroutines();
            a = coroutines.a;
            bins = coroutines.bins;
        }

        // Update is called once per frame
        void Update()
        {
            ref crValues value = ref a.GetNext();
            while (value.enumerator != null)
            {
                value.remainingTime -= Time.deltaTime;
                if (value.remainingTime <= 0F)
                {
                    if (value.enumerator.MoveNext())
                    {
                        value.remainingTime = value.enumerator.Current;
                    }
                    else
                    {
                        a.DeleteAtIterator();
                    }
                }
                value = ref a.GetNext();
            }
            int i = 0;
            do
            {
                batch b = bins[i];
                ref crValues val = ref b.GetNext();
                while (val.enumerator != null)
                {
                    val.remainingTime -= Time.deltaTime;
                    if (val.remainingTime <= 0F)
                    {
                        if (val.enumerator.MoveNext())
                        {
                            val.remainingTime = val.enumerator.Current;
                        }
                        else
                        {
                            b.DeleteAtIterator();
                        }
                    }
                    val = ref b.GetNext();
                }
            }
            while (++i < 5);
        }
    }

    struct CoroutineHandle
    {
        public batch bin;
        public batch.added access;
        public int index;
    }
}

Video:

1 Like

Iā€™m aware this is not DOTS but performance-related for users who use the classic way.

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.

Overhead results in longer frametime (no vsync or ā€¦). But number of frames is number of yields returning ā€ž0ā€œ plus those during waiting for seconds.

1 Like