通用工厂:缓存与重复实例化

发布于 2024-11-10 18:52:47 字数 409 浏览 5 评论 0原文

我有一个通用工厂,它在返回实例之前对其进行缓存(简化的代码):

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

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

我想用非缓存方法替换这种方法,以表明缓存在实例化性能方面没有任何意义(我相信创建新对象非常便宜) 。

所以我想编写一个负载测试,它将创建一个动态的、仅运行时类型的交易,比如 1000 个交易,并将其加载到我的工厂中。一个会缓存,另一个则不会。

I have a generic factory which caches an instance before return it (simplified 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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

云胡 2024-11-17 18:52:47

尽管我同意 jgauffin 和 Daniel Hilgarth 的回答,但这是我的两点意见。以这种方式使用静态成员的泛型类型缓存将直观地为每个缓存类型创建额外的并行类型,但重要的是要了解这对于引用类型和值类型的工作方式有何不同。对于 T 等引用类型,生成的附加泛型类型应使用比值类型的等效使用更少的资源。

那么什么时候应该使用泛型类型技术来生成缓存呢?以下是我使用的一些重要标准。
1. 您希望允许缓存每个感兴趣的类的单个实例。
2. 您希望使用编译时泛型类型约束来对缓存中使用的类型强制执行规则。通过类型约束,您可以强制实例实现多个接口,而无需为这些类定义基类型。
3. 在 AppDomain 的生命周期内,您不需要从缓存中删除项目。

顺便说一下,搜索可能有用的一个术语是“代码爆炸”,它是一个通用术语,用于定义需要大量代码来执行某些定期发生的任务并且通常会线性增长或更糟的情况项目要求。就泛型类型而言,我听说过并且通常会使用术语“类型爆炸”来描述当您开始组合和组合多个泛型类型时类型的扩散。

另一个重要的一点是,在这些情况下,工厂和缓存始终可以分开,并且在大多数情况下,可以为它们提供相同的接口,这将允许您替换工厂(每次调用的新实例)或本质上包装工厂的缓存如果您想根据类型爆炸问题等因素使用其中一种或另一种,则可以通过相同的接口进行委托。您的缓存还可以承担更多责任,例如更复杂的缓存策略,其中可能以不同的方式缓存特定类型(例如引用类型与值类型)。如果您对此感到好奇,技巧是定义泛型类,该类将缓存作为实现工厂接口的实际具体类型中的私有类。如果你愿意的话我可以举一个例子。

根据要求更新示例代码:

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;
        }
    }
}

基本上总结一下,上面的代码是一个控制台应用程序,它具有提供服务源抽象的接口的概念。我使用 IService 通用约束只是为了展示它的重要性的示例。我不想键入或发布 1000 个单独的类型定义,因此我做了下一个最好的事情,创建了三个类 - A、B 和 C - 并使用每种技术访问它们各 1000 次 - 重复实例化、泛型类型缓存和基于集合的缓存。

对于一小组访问,差异可以忽略不计,但当然我的服务构造函数很简单(默认无参数构造函数),因此它不会计算任何内容、访问数据库、访问配置或典型服务类在构造时执行的任何操作。如果情况并非如此,那么某些缓存策略的好处显然将有利于性能。此外,即使在访问次数为 1,000,000 次的 caes 中访问默认构造函数时,不缓存和缓存之间仍然存在显着差异(3s:120ms),因此教训是,如果您正在进行大量访问或需要频繁访问的复杂计算通过工厂,缓存不仅是有益的,而且几乎是必要的,具体取决于它是否影响用户感知或时间敏感的业务流程,否则好处可以忽略不计。需要记住的重要一点是,您不仅需要担心实例化时间,还需要担心垃圾收集器的负载。

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.

会傲 2024-11-17 18:52:47

在我看来,你的同事想要进行过早的优化。缓存对象很少是一个好主意。实例化很便宜,我只会缓存已证明会更快的对象。高性能套接字服务器就是这种情况。

但回答你的问题:缓存对象总是会更快。将它们保存在 LinkedList 或类似的东西中将使开销保持较小,并且性能不会随着对象数量的增长而下降。

因此,如果您愿意接受更大的内存消耗和增加的复杂性,请选择缓存。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文