2011-05-31 1 views
1

J'ai une usine générique qui met en cache une instance de retour avant qu'il (code simplifié):usine générique: cache par rapport à instantination répétitif

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

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

Je veux remplacer cette approche de non-mise en cache un pour montrer que la mise en cache n'a aucun sens en matière de performance d'instanciation (je crois que la création de nouveaux objets est très bon marché). Donc, je veux écrire un test de charge qui va créer un deal, par exemple 1000, de types dynamiques, à l'exécution seulement et le charger dans mes usines. L'un mettra en cache et l'autre ne le fera pas.

Répondre

1

Cela me semble que votre collègue veut faire des optimisations prématurées. La mise en cache d'objets est rarement une bonne idée. L'instanciation est bon marché et je ne ferais que mettre en cache des objets où il est prouvé que ce sera plus rapide. Un serveur socket haute performance serait un tel cas. Mais pour répondre à votre question: La mise en cache des objets sera toujours plus rapide. En les gardant dans un LinkedList ou quelque chose comme ça, cela réduira les frais généraux et les performances ne devraient pas diminuer à mesure que le nombre d'objets augmente.

Donc, si vous êtes prêt à accepter une plus grande consommation de mémoire et une complexité accrue, optez pour un cache.

+0

Merci de votre réponse, pour avoir partagé votre opinion. btw, la mise en cache est effectuée automatiquement par CLR ('private T'). Je cherche donc un moyen de mesurer le temps de recherche du cache, la taille du cache et le temps d'instanciation. Juste pour avoir la preuve matérielle d'un des points de vue. – abatishchev

+0

Dépend du conteneur. Si vous voulez juste obtenir le premier élément mis en cache, le temps de recherche est O (1). – jgauffin

+0

Je n'ai pas de conteneur, c'est effectué derrière la scène, par CLR. Qu'est-ce qu'il utilise - je ne peux pas imaginer. Cependant, ce serait intéressant. – abatishchev

2

Voici mes deux cents, bien que je sois d'accord avec les réponses de jgauffin et Daniel Hilgarth. L'utilisation de la mise en cache de type générique en utilisant un membre statique de cette manière créerait intuitivement des types parallèles supplémentaires par type mis en cache, mais il est important de comprendre comment cela fonctionne différemment pour les types de référence et de valeur. Pour les types de référence tels que T, les types génériques supplémentaires produits doivent utiliser moins de ressources que l'utilisation équivalente d'un type de valeur.

Alors, quand devriez-vous utiliser la technique du type générique pour produire un cache? Voici quelques critères importants que j'utilise. 1. Vous souhaitez autoriser la mise en cache d'instances uniques de chaque classe d'intérêt. 2. Vous souhaitez utiliser des contraintes de type générique de compilation pour appliquer des règles sur les types utilisés dans le cache. Avec les contraintes de type, vous pouvez imposer une instance pour implémenter plusieurs interfaces sans avoir à définir un type de base pour ces classes. 3. Vous n'avez pas besoin de supprimer des éléments du cache pour la durée de vie de l'AppDomain. Par ailleurs, un terme qui peut être utile à rechercher est "Code Explosion" qui est un terme général utilisé pour définir les cas où une quantité considérable de code est nécessaire pour effectuer une tâche régulière et qui se développe généralement linéairement ou pire avec la croissance des exigences du projet. En termes de types génériques, j'ai entendu et j'utiliserai généralement le terme «explosion de type» pour décrire la prolifération de types lorsque vous commencez à combiner et à composer plusieurs types génériques. Un autre point important est que dans ces cas, une fabrique et le cache peuvent toujours être séparés et dans la plupart des cas, ils peuvent recevoir une interface identique qui vous permet de remplacer l'usine (nouvelle instance par appel) ou la mémoire cache essentiellement enveloppe l'usine et les délégués à travers la même interface dans les cas où vous souhaitez utiliser l'un ou l'autre en fonction de choses telles que les problèmes d'explosion de type. Votre cache peut également prendre plus de responsabilités, comme une stratégie de cache plus sophistiquée où des types particuliers sont mis en cache différemment (par exemple, les types de référence et les types de valeur). Si votre curiosité à ce sujet l'astuce est de définir votre classe générique qui fait la mise en cache en tant que classe privée dans le type concret réel qui implémente l'interface pour votre usine. Je peux donner un exemple si vous le souhaitez.

Mise à jour avec le code exemple comme demandé:

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

En fait, pour résumer, le code ci-dessus est une application console qui a le concept d'une interface pour assurer une abstraction d'une source de service. J'ai utilisé une contrainte générique IService juste pour montrer un exemple de comment cela pourrait avoir de l'importance. Je ne veux pas taper ou publier 1000 définitions de types séparés, donc j'ai fait la meilleure chose suivante et créé trois classes - A, B et C - et y accédé 1000 fois en utilisant chaque technique - instanciation répétitive, cache de type générique, et cache basé sur la collection. Avec un petit nombre d'accès, la différence est négligeable, mais mon constructeur de services est simpliste (constructeur sans paramètre par défaut) donc il ne calcule rien, accède à une base de données, accède à la configuration ou à tout autre chose. quand ils sont construits. Si ce n'était pas le cas, les avantages d'une stratégie de mise en cache vont évidemment être bénéfiques pour la performance. Même lorsque vous accédez au constructeur par défaut dans les cés où il y a 1 000 000 d'accès, la différence entre la mise en cache et la mise en cache (3s: 120ms) reste importante: si vous effectuez des accès à haut volume ou des calculs complexes nécessitant un accès fréquent À travers l'usine, la mise en cache sera non seulement bénéfique mais proche de la nécessité, selon qu'elle aura un impact sur la perception de l'utilisateur ou sur les processus métier sensibles au temps, sans quoi les bénéfices seront négligeables. La chose importante à retenir est que ce n'est pas seulement le temps d'instanciation que vous devez vous inquiéter, mais aussi la charge sur le Garbage collector.

+0

Un exemple de scénario générique complexe est toujours le bienvenu :) – abatishchev

Questions connexes