开发者

Generic factory: cache vs. repetitive instantination

I have a generic factory which caches an instance before return it (开发者_StackOverflowsimplified code):

static class Factory<T>
    where T : class
{
    private static T instance;

    public static T GetInstance()
    {
        if (instance == null) instance = new T();
        return instance;
    }
}

I want to replace such approach with non-caching one to show that caching makes no sense in matters of instantiation performance (I believe new object creation is very cheap).

So I want to write a load test which will create a deal, say 1000, of dynamic, runtime-only types and load it to my factories. One will cache, and another - will not.


Here's my two cents although I agree with jgauffin's and Daniel Hilgarth's answers. Using generic type caching using a static member in this way would intuitively create additional parallel types per type that is cached but it is important to understand how this works differently for reference and value types. For reference types as T the additional generic types produced should use less resources than would an equivalent usage of a value type.

So when should you use the generic type technique for producing a cache? Below are a few important criteria that I use. 1. You want to allow caching single instances of each class of interest. 2. You would like to use compile time generic type constraints to enforce rules on the types used in the cache. With type constraints you can enforce the need for an instance to implement several interfaces without having to define a base type for those classes. 3. You don't need to remove items from the cache for the lifetime of the AppDomain.

By the way one term that may be useful to search on is "Code Explosion" which is a general term used to define cases where a considerable amount of code is needed to perform some regularly occurring task and that generally grows linearly or worse with the growth of project requirements. In terms of generic types, I've heard and will generally use the term "type explosion" to describe the proliferation of types as you begin to combine and compose several generic types.

Another important point is that in these cases a factory and the cache can always be separated and in most cases they can be given an identical interface which would allow you to substitute the factory (new instance per call) or the cache which essentially wraps the factory and delegates through the same interface in cases where you want to use one or the other depending on things such as type explosion concerns. Your cache could also take on more responsibility such as a more sophisticated caching strategy where perhaps particular types are cached differently (ex. reference types vs. value types). If your curious about this the trick is to define your generic class that does the caching as a private class within the actual concrete type that implements the interface for your factory. I can give an example if you would like.

Update with example code as requested:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace CacheAndFactory
{
    class Program
    {
        private static int _iterations = 1000;

        static void Main(string[] args)
        {
            var factory = new ServiceFactory();

            // Exercise the factory which implements IServiceSource
            AccessAbcTwoTimesEach(factory);

            // Exercise the generics cache which also implements IServiceSource
            var cache1 = new GenericTypeServiceCache(factory);
            AccessAbcTwoTimesEach(cache1);

            // Exercise the collection based cache which also implements IServiceSource
            var cache2 = new CollectionBasedServiceCache(factory);
            AccessAbcTwoTimesEach(cache2);

            Console.WriteLine("Press any key to continue");
            Console.ReadKey();
        }

        public static void AccessAbcTwoTimesEach(IServiceSource source)
        {
            Console.WriteLine("Excercise " + source.GetType().Name);

            Console.WriteLine("1st pass - Get an instance of A, B, and C through the source and access the DoSomething for each.");
            source.GetService<A>().DoSomething();
            source.GetService<B>().DoSomething();
            source.GetService<C>().DoSomething();
            Console.WriteLine();

            Console.WriteLine("2nd pass - Get an instance of A, B, and C through the source and access the DoSomething for each.");
            source.GetService<A>().DoSomething();
            source.GetService<B>().DoSomething();
            source.GetService<C>().DoSomething();
            Console.WriteLine();

            var clock = Stopwatch.StartNew();

            for (int i = 0; i < _iterations; i++)
            {
                source.GetService<A>();
                source.GetService<B>();
                source.GetService<C>();
            }

            clock.Stop();

            Console.WriteLine("Accessed A, B, and C " + _iterations + " times each in " + clock.ElapsedMilliseconds + "ms through " + source.GetType().Name + ".");
            Console.WriteLine();
            Console.WriteLine();
        }
    }

    public interface IService
    {
    }

    class A : IService
    {
        public void DoSomething() { Console.WriteLine("A.DoSomething(), HashCode: " + this.GetHashCode()); }
    }

    class B : IService
    {
        public void DoSomething() { Console.WriteLine("B.DoSomething(), HashCode: " + this.GetHashCode()); }
    }

    class C : IService
    {
        public void DoSomething() { Console.WriteLine("C.DoSomething(), HashCode: " + this.GetHashCode()); }
    }

    public interface IServiceSource
    {
        T GetService<T>() 
            where T : IService, new();
    }

    public class ServiceFactory : IServiceSource
    {
        public T GetService<T>() 
            where T : IService, new()
        {
            // I'm using Activator here just as an example
            return Activator.CreateInstance<T>();
        }
    }

    public class GenericTypeServiceCache : IServiceSource
    {
        IServiceSource _source;

        public GenericTypeServiceCache(IServiceSource source)
        {
            _source = source;
        }

        public T GetService<T>() 
            where T : IService, new()
        {
            var serviceInstance = GenericCache<T>.Instance;
            if (serviceInstance == null)
            {
                serviceInstance = _source.GetService<T>();
                GenericCache<T>.Instance = serviceInstance;
            }

            return serviceInstance;
        }

        // NOTE: This technique will cause all service instances cached here 
        // to be shared amongst all instances of GenericTypeServiceCache which
        // may not be desireable in all applications while in others it may
        // be a performance enhancement.
        private class GenericCache<T>
        {
            public static T Instance;
        }
    }

    public class CollectionBasedServiceCache : IServiceSource
    {
        private Dictionary<Type, IService> _serviceDictionary;
        IServiceSource _source;

        public CollectionBasedServiceCache(IServiceSource source)
        {
            _serviceDictionary = new Dictionary<Type, IService>();
            _source = source;
        }

        public T GetService<T>()
            where T : IService, new()
        {

            IService serviceInstance;
            if (!_serviceDictionary.TryGetValue(typeof(T), out serviceInstance))
            {
                serviceInstance = _source.GetService<T>();
                _serviceDictionary.Add(typeof(T), serviceInstance);
            }

            return (T)serviceInstance;
        }

        private class GenericCache<T>
        {
            public static T Instance;
        }
    }
}

Basically to summarize, the code above is a console app that has the concept of an interface to provide for an abstraction of a service source. I used an IService generic constraint just to show an example of how it could matter. I don't want to type or post 1000 separate type definitions so I did the next best thing and created three classes - A, B, and C - and accessed them each 1000 times using each technique - repetitive instantiation, generic type cache, and collection based cache.

With a small set of accesses the difference is negligible but of course my service constructor is simplistic (default parameterless constructor) so it does not calculate anything, access a database, access configuration or any of the things that typical service classes do when they are constructed. If this were not the case then the benefits of some caching strategy is obviously going to be beneficial for performance. Also when accessing even the default constructor in the caes where there are 1,000,000 accesses there is still a dramatic difference between not caching and caching (3s : 120ms) so the lesson is that if you are doing high volume accesses or complex calculations that require frequent access through the factory then caching will be not only beneficial but verging on a necessity depending on whether it impacts user perception or time sensitive business processes otherwise the benefits are negligible. The important thing to remember is that it's not just instantiation time that you have to worry about but also the load on the Garbage collector.


Sounds to me that your colleague want's to do premature optimizations. Caching objects are seldom a good idea. Instantiation is cheap and I would only cache objects where it's proven that it will be faster. A high performance socket server would be such case.

But to answer your question: Caching objects will always be faster. Keeping them in a LinkedList or something like that will keep the overhead small and performance should not decrease as the number of objects grow.

So if you are willing to accept larger memory consumption and increased complexity, go for a cache.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜