Konstruowanie obiektów IRandomizer oraz testy (wydajności)

W poprzednim odcinku dokonałem wyabstrahowania interfejsu IRandomization by umożliwić testowanie kodu korzystającego z elementów losowości. W obecnym skupię się na podstawowych obiektach konstrukcyjnych. Kłania się wzorzec fabryki abstrakcyjnej.

public interface IRandomizerBuilder
{
    IRandomizer ConstructDeafultRandomizer();

    IRandomizer ConstructRandomizer(int seed);
}


Podstawowa implementacja tej fabryki na potrzeby standardowego wykorzystania w programie jest banalna:

public class SystemRandomizerBuilder : IRandomizerBuilder
{
    public IRandomizer ConstructDeafultRandomizer()
    {
        return new SystemRnd(new Random());
    }

    public IRandomizer ConstructRandomizer(int seed)
    {
        return new SystemRnd(new Random(seed));
    }
}

Łatwo się domyślić, że fabryka deserializująca będzie nieznacznie tylko bardziej skomplikowana, ale o tym kiedy indziej.

W następnej kolejności postanowiłem dorobić obiekt fejkowy, którego planuję użyć do testowania rozmaitych przypadków użycia:

public class FakeRandom// : Random
{
    private readonly int[] _fakeSequence;
    private int _index;

    public FakeRandom(int[] fakeSequence)
    {
        if (fakeSequence == null) throw new ArgumentNullException(nameof(fakeSequence));
        if (fakeSequence.Length == 0)
            throw new ArgumentException("Argument is empty collection", nameof(fakeSequence));

        _fakeSequence = fakeSequence;
        _index = 0;
    }

    public int GetNext()
    {
        try
        {
            return _fakeSequence[_index];
        }
        finally
        {
            CheckIndex();
        }
    }

    internal int GetNext(int max)
    {
        try
        {
            int value = _fakeSequence[_index];
            if (value >= max)
            {
                throw new TestException("Value in FakeRandom sequence is greater than expected GetNext(max) boundary.");
            }
            return value;
        }
        finally
        {
            CheckIndex();
        }
    }

    internal int GetNext(int min, int max)
    {
        try
        {
            int value = _fakeSequence[_index];
            if (value < min)
            {
                throw new TestException("Value in FakeRandom sequence is lower than expected GetNext(min, max) boundary.");
            }
            if (value >= max)
            {
                throw new TestException("Value in FakeRandom sequence is greater than expected GetNext(min, max) boundary.");
            }
            return value;
        }
        finally
        {
            CheckIndex();
        }
    }

    public double GetNextDouble()
    {
        try
        {
            return (double)_fakeSequence[_index];
        }
        finally
        {
            CheckIndex();
        }
    }

    private void CheckIndex()
    {
        _index++;
        if (_index >= _fakeSequence.Length)
        {
            _index = 0;
        }
    }
}

Jak widać oparłem klasę testującą o cykliczną tablicę liczb, które ma zwracać. Dostarczenie jej poprawnej zawartości (ja jak zwykle umywam ręce ;-)) należy już do kodu testującego, który powinien dokonać tego za pomocą odpowiedniej implementacji fabryki:

public class FakeRandomizerBuilder : IRandomizerBuilder
{
    private readonly int[] _fakeSequence;

    public FakeRandomizerBuilder(params int[] fakeSequence)
    {
        _fakeSequence = fakeSequence;
    }

    public IRandomizer ConstructDeafultRandomizer()
    {
        return new FakeRnd(new FakeRandom(_fakeSequence));
    }

    public IRandomizer ConstructRandomizer(int seed)
    {
        return new FakeRnd(new FakeRandom(_fakeSequence));
    }
}

Czas na pierwszy test – sprawdza jednocześnie poprawność działania tej architektury, jak i porównuje jej wydajność:

[TestFixture]
public class RandomizersInfrastructureTests
{
    [Test]
    public void CompareCleanPerformanceVsInfrastructure()
    {
        Random pureImpl = new Random(0);

        FakeRandomizerBuilder fakeImplBuilder = new FakeRandomizerBuilder(0); // short
        var fakeimpl = fakeImplBuilder.ConstructRandomizer(0);

        SystemRandomizerBuilder sysBuilder = new SystemRandomizerBuilder();
        var sysImpl = sysBuilder.ConstructRandomizer(0);

        int testQuantity = 10000000;

        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < testQuantity; i++)
        {
            int r = pureImpl.Next();
        }
        sw.Stop();
        long pureMs = sw.ElapsedMilliseconds;

        sw.Restart();
        for (int i = 0; i < testQuantity; i++)
        {
            int r = fakeimpl.GetNext();
        }
        sw.Stop();
        long fakemMs = sw.ElapsedMilliseconds;

        sw.Restart();
        for (int i = 0; i < testQuantity; i++)
        {
            int r = sysImpl.GetNext();
        }
        sw.Stop();
        long sysMs = sw.ElapsedMilliseconds;

        Console.WriteLine("Times compare(ms): {0}(PURE), {1}(FAKE), {2}(SYS)",
            pureMs, fakemMs, sysMs);
    }    
}

I wyniki dla powyższych 10 milionów prób. Niefajne:

Times compare(ms): 117(PURE), 378(FAKE), 356(SYS) - single run on laptop
Times compare(ms): 1057(PURE), 2089(FAKE), 1793(SYS) - profiling on laptop
Times compare(ms): 80(PURE), 105(FAKE), 114(SYS) - monster (release)
Times compare(ms): 94(PURE), 377(FAKE), 357(SYS) - monster (debug)
Times compare(ms): 928(PURE), 1900(FAKE), 2106(SYS) - monster, debug, profiling

Jak widać ogromny wpływ podłączonego profilera (dziesięciokrotne spowolnienie), oraz niemal dwukrotne spowolnienie działania wersji kodu w wersji debug dla całej abstrakcji. w wersji release nie jest już tak źle – różnica wynosi około 30-40%. Przypuszczam, głównie na wywołaniach funkcji wirtualnych. Profiler niestety przy tak ciasnych obszarach niestety nie jest zbyt pomocny:

Rnd profiling

Póki co zostawiam jak jest, nie jest to mimo wszystko jakaś problematyczna różnica – nawet w najbardziej wymagających scenariuszach nie przewiduję więcej niż kilkaset odwołań do generatora na sekundę. A nie do przecenienia będzie możliwość łatwego śledzenia zdarzeń z nim związanych.