Search code examples
c#c++delphilazy-initializationself-modifying

Remove null check after lazy initialization


When one decides to use lazy initialization, he usually has to pay for it.

class Loafer
{
    private VeryExpensiveField field;
    private VeryExpensiveField LazyInitField()
    {
        field = new VeryExpensiveField();
        // I wanna here remove null check from accessor, but how?
        return field;
    }
    property Field { get { return field ?? LazyInitField(); } }
}

Basically, he has to check every time if his backing field has null/nil value. What if he could escape from this practice? When you successfully initialize the field, you can rid of this check, right?

Unfortunately, majority of production languages do not allow you to modify their functions in run-time, especially add or remove single instructions from the function body though it would be helpful if used wisely. However, in C#, you can use delegates (initially I discovered them and afterwards realized why native languages having function pointers for) and events mechanism to imitate such behavior with consequent lack of performance, because null-checks just move onto lower level, but do not disappear completely. Some languages, e.g. LISP and Prolog, allow you to modify their code easily, but they are hardly can be treated as production languages.

In native languages like Delphi and C/C++ it seems better to write two functions, safe and rapid, call them by pointer and switch this pointer to rapid version after initialization. You can even allow compiler or IDE to generate code to do this without additional headache. But as @hvd mentioned, this can even decrease speed, because CPU will not know that those functions are almost the same, thus will not prefetch them into it's cache.

Yes, I'm performance maniac seeking for performance without explicit problem, just to feed my curiosity. What common approaches are exist to develop such functionality?


Solution

  • Actually the laziness toolkit framework is not always that important, when you compare it's overhead to the actual computation.

    There are many approaches. You can use Lazy, a self modifying lambda setup, a boolean or whatever suits your workflow best.

    Lazy evaluation toolkit's overhead is only important to consider when you have some repeated computation.

    My code example with a micro benchmark explores the comparative overhead of lazy computation in context of an accompanying more expensive operation in a loop.

    You can see that laziness toolkit's overhead is neglectible even when used along with a relatively chip payload operation.

    void Main()
    {
        // If the payload is small, laziness toolkit is not neglectible
        RunBenchmarks(i => i % 2 == 0, "Smaller payload");
    
        // Even this small string manupulation neglects overhead of laziness toolkit
        RunBenchmarks(i => i.ToString().Contains("5"), "Larger payload");
    }
    
    void RunBenchmarks(Func<int, bool> payload, string what)
    {
        Console.WriteLine(what);
        var items = Enumerable.Range(0, 10000000).ToList();
    
        Func<Func<int, bool>> createPredicateWithBoolean = () =>
        {
            bool computed = false;
            return i => (computed || (computed = Compute())) && payload(i);
        };
    
        items.Count(createPredicateWithBoolean());
        var sw = Stopwatch.StartNew();
        Console.WriteLine(items.Count(createPredicateWithBoolean()));
        sw.Stop();
        Console.WriteLine("Elapsed using boolean: {0}", sw.ElapsedMilliseconds);
    
        Func<Func<int, bool>> createPredicate = () =>
        {
            Func<int, bool> current = i =>
            {
                var computed2 = Compute();
                current = j => computed2;
                return computed2;
            };
            return i => current(i) && payload(i);
        };
    
        items.Count(createPredicate());
        sw = Stopwatch.StartNew();
        Console.WriteLine(items.Count(createPredicate()));
        sw.Stop();
        Console.WriteLine("Elapsed using smart predicate: {0}", sw.ElapsedMilliseconds);
        Console.WriteLine();
    }
    
    bool Compute()
    {
        return true; // not important for the exploration
    }
    

    Output:

    Smaller payload
    5000000
    Elapsed using boolean: 161
    5000000
    Elapsed using smart predicate: 182
    
    Larger payload
    5217031
    Elapsed using boolean: 1980
    5217031
    Elapsed using smart predicate: 1994