DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 3.31 Creating an Object Cache

Problem

Your application creates many objects that are expensive to create and/or have a large memory footprint—for instance, objects that are populated with data from a database or a web service upon their creation. These objects are used throughout a large portion of the application's lifetime. You need a way to not only enhance the performance of these objects—and as a result, your application—but also to use memory more efficiently.

Solution

Create an object cache to keep these objects in memory as long as possible, without tying up valuable heap space and possibly resources. Since cached objects may be reused at a later time, you also forego the process of having to create similar objects many times.

You can reuse the ASP.NET cache that is located in the System.Web.Caching namespace or you can build your own lightweight caching mechanism. The See Also section at the end of this recipe provides several Microsoft resources that show you how to use the ASP.NET cache to cache your own objects. However, the ASP.NET cache is very complex and may have a nontrivial overhead associated with it, so using a lightweight caching mechanism like the one shown here is a viable alternative.

The following class, ObjCache, represents a type that allows the caching of SomeComplexObj objects:

using System;
using System.Collections;

public class ObjCache
{
    // Constructors
    public ObjCache( )
    {
        Cache = new Hashtable( );
    }

    public ObjCache(int initialCapacity)
    {
        Cache = new Hashtable(initialCapacity);
    }

    // Fields
    private Hashtable cache = null;

    // Methods
    public SomeComplexObj GetObj(object key)
    {
        if (!cache.ContainsKey(key) || !IsObjAlive(key))
        {
            AddObj(key, new SomeComplexObj( ));
        }

        return ((SomeComplexObj)((WeakReference)cache[key]).Target);
    }

    public object GetObj(object key, object obj)
    {
        if (!cache.ContainsKey(key) || !IsObjAlive(key))
        {
            return (null);
        }
        else
        {
            return (((WeakReference)cache[key]).Target);
        }
    }

    public void AddObj(object key, SomeComplexObj item)
    {
        WeakReference WR = new WeakReference(item, false);

        if (cache.ContainsKey(key))
        {
            cache[key] = WR;
        }
        else
        {
            cache.Add(key, WR);
        }
    }

    public void AddObj(object key, object item)
    {
        WeakReference WR = new WeakReference(item, false);

        if (cache.ContainsKey(key))
        {
            cache[key] = WR;
        }
        else
        {
            cache.Add(key, WR);
        }
    }

    public bool IsObjAlive(object key)
    {
        if (cache.ContainsKey(key))                                                
        {
            return (((WeakReference)cache[key]).IsAlive);
        }
        else
        {
            return (false);
        }
    }

    public int AliveObjsInCache( )
    {
        int count = 0;

        foreach (DictionaryEntry item in cache)
        {
            if (((WeakReference)item.Value).IsAlive)
            {
                count++;
            }
        }

        return (count);
    }

    public int ExistsInGeneration(object key)
    {
        int retVal = -1;

        if (cache.ContainsKey(key) && IsObjAlive(key))
        {
            retVal = GC.GetGeneration((WeakReference)cache[key]);
        }

        return (retVal);
    }

    public bool DoesKeyExist(object key)
    {
        return (cache.ContainsKey(key));
    }

    public bool DoesObjExist(object complexObj)
    {
        return (cache.ContainsValue(complexObj));
    }

    public int TotalCacheSlots( )
    {
        return (cache.Count);
    }
}

The SomeComplexObj class can be replaced with any type of class you choose. For this recipe, we will use this class, but for your code, you can change it to whatever class or structure type you need.


The SomeComplexObj is defined here (realistically, this would be a much more complex object to create and use; however, for the sake of brevity, this class is written as simply as possible):

public class SomeComplexObj
{
    public SomeComplexObj( )        {}

    private int idcode = -1;

    public int IDCode
    {
        set{idcode = value;}
        get{return (idcode);}
    }
}

ObjCache, the caching object used in this recipe, makes use of a Hashtable object to hold all cached objects. This Hashtable allows for fast lookup when retrieving objects and generally for fast insertion and removal times. The Hashtable object used by this class is defined as a private field and is initialized through its overloaded constructors.

Developers using this class will mainly be adding and retrieving objects from this object. The GetObj method implements the retrieval mechanism for this class. This method returns a cached object if its key exists in the Hashtable and the WeakReference object is considered to be alive. An object that the WeakReference type refers to has not been garbage collected. The WeakReference type can remain alive long after the object to which it referred is gone. An indication of whether this WeakReference object is alive is obtained through the read-only IsAlive property of the WeakReference object. This property returns a bool indicating whether this object is alive (true) or not (false). When an object is not alive, or when its key does not exist in the Hashtable, this method creates a new object with the same key as the one passed in to the GetObj method and adds it to the Hashtable.

The AddObj method implements the mechanism to add objects to the cache. This method creates a WeakReference object that will hold a weak reference to our object. Each object in the cache is contained within a WeakReference object. This is the core of the caching mechanism used in this recipe. A WeakReference that references an object (its target) allows that object to later be referenced through itself. When the target of the WeakReference object is also referenced by a strong (i.e., normal) reference, the GC cannot collect the target object. But if no references are made to this WeakReference object, the GC can collect this object to make room in the managed heap for new objects.

After creating the WeakReference object, the Hashtable is searched for the same key that we want to add. If an object with that key exists, it is overwritten with the new object; otherwise, the Add method of the Hashtable class is called.

The ObjCache class has been written to cache either a specific object type or multiple object types. To do this, a method called GetAnyTypeObj has been added that returns an object. Additionally, the AddObj method is overloaded to accept an object as its second parameter type. The following code uses the strongly typed GetObj method to return a SomeComplexObj object:

SomeComplexObj SCO2 = OC.GetObj("ID2");

The following code uses the generic GetAnyTypeObj method to return some other type of object:

Obj SCO2 = (Obj)OC.GetAnyTypeObj("ID2");
if (SCO2 == null)
{
    OC.AddObj("ID2", new Obj( ));
    SCO2 = (Obj)OC.GetAnyTypeObj("ID2");
}

where Obj is an object of any type. Notice that it is now the responsibility of the caller to verify that the GetObj method does not return null.

Quite a bit of extra work is required in the calling code to support a cache of heterogeneous objects. More responsibility is placed on the user of this cache object, which can quickly lead to usability and maintenance problems if not written correctly.

The code to exercise the ObjCache class is shown here:

// Create the cache here
ObjCache OC = new ObjCache( );

public void TestObjCache( )
{
    OC.AddObj("ID1", new SomeComplexObj( ));
    OC.AddObj("ID2", new SomeComplexObj( ));
    OC.AddObj("ID3", new SomeComplexObj( ));
    OC.AddObj("ID4", new SomeComplexObj( ));
    OC.AddObj("ID5", new SomeComplexObj( ));

    Console.WriteLine("\r\n--> Add 5 weak references");
    Console.WriteLine("OC.TotalCacheSlots = " + OC.TotalCacheSlots( ));
    Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( ));
    Console.WriteLine("OC.ExistsInGeneration('ID1') = " + 
      OC.ExistsInGeneration("ID1"));

    ////////////// BEGIN COLLECT //////////////
    GC.Collect( );
    GC.WaitForPendingFinalizers( );
    //////////////  END COLLECT  //////////////

    Console.WriteLine("\r\n--> Collect all weak references");
    Console.WriteLine("OC.TotalCacheSlots = " + OC.TotalCacheSlots( ));
    Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( ));

    OC.AddObj("ID1", new SomeComplexObj( ));
    OC.AddObj("ID2", new SomeComplexObj( ));
    OC.AddObj("ID3", new SomeComplexObj( ));
    OC.AddObj("ID4", new SomeComplexObj( ));
    OC.AddObj("ID5", new SomeComplexObj( ));

    Console.WriteLine("\r\n--> Add 5 weak references");
    Console.WriteLine("OC.TotalCacheSlots = " + OC.TotalCacheSlots( ));
    Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( ));

    CreateObjLongMethod( );
    Create135( );
    CollectAll( );
}

private void CreateObjLongMethod( )
{
    Console.WriteLine("\r\n--> Obtain ID1");
    if (OC.IsObjAlive("ID1"))
    {
        SomeComplexObj SCOTemp = OC.GetObj("ID1");
        SCOTemp.IDCode = 100;
        Console.WriteLine("SCOTemp.IDCode = " + SCOTemp.IDCode);
    }
    else
    {
        Console.WriteLine("Object ID1 does not exist...Creating new ID1...");
        OC.AddObj("ID1", new SomeComplexObj( ));                                

        SomeComplexObj SCOTemp = OC.GetObj("ID1");
        SCOTemp.IDCode = 101;
        Console.WriteLine("SCOTemp.IDCode = " + SCOTemp.IDCode);
    }
}

private void Create135( )
{
    Console.WriteLine("OC.ExistsInGeneration('ID1') = " + 
      OC.ExistsInGeneration("ID1"));
    Console.WriteLine("\r\n--> Obtain ID1, ID3, ID5");
    SomeComplexObj SCO1 = OC.GetObj("ID1");
    SomeComplexObj SCO3 = OC.GetObj("ID3");
    SomeComplexObj SCO5 = OC.GetObj("ID5");
    SCO1.IDCode = 1000;
    SCO3.IDCode = 3000;
    SCO5.IDCode = 5000;
    Console.WriteLine("OC.ExistsInGeneration('ID1') = " + 
      OC.ExistsInGeneration("ID1"));

    ////////////// BEGIN COLLECT //////////////
    GC.Collect( );
    GC.WaitForPendingFinalizers( );
    //////////////  END COLLECT  //////////////

    Console.WriteLine("\r\n--> Collect all weak references");
    Console.WriteLine("OC.TotalCacheSlots = " + OC.TotalCacheSlots( ));
    Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( ));
    Console.WriteLine("OC.ExistsInGeneration('ID1') = " 
      + OC.ExistsInGeneration("ID1"));

    Console.WriteLine("SCO1.IDCode = " + SCO1.IDCode);
    Console.WriteLine("SCO3.IDCode = " + SCO3.IDCode);
    Console.WriteLine("SCO5.IDCode = " + SCO5.IDCode);

    Console.WriteLine("\r\n--> Get ID2, which has been collected.  ID2 Exists ==" +     
      OC.IsObjAlive("ID2"));
    SomeComplexObj SCO2 = OC.GetObj("ID2");
    Console.WriteLine("ID2 has now been re-created.  ID2 Exists == " +
      OC.IsObjAlive("ID2"));
    Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( ));
    SCO2.IDCode = 2000;
    Console.WriteLine("SCO2.IDCode = " + SCO2.IDCode);

    ////////////// BEGIN COLLECT //////////////
    GC.Collect( );
    GC.WaitForPendingFinalizers( );
    //////////////  END COLLECT  //////////////

    Console.WriteLine("\r\n--> Collect all weak references");
    Console.WriteLine("OC.TotalCacheSlots = " + OC.TotalCacheSlots( ));
    Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( ));
    Console.WriteLine("OC.ExistsInGeneration('ID1') = " + 
      OC.ExistsInGeneration("ID1"));
    Console.WriteLine("OC.ExistsInGeneration('ID2') = " + 
      OC.ExistsInGeneration("ID2"));
    Console.WriteLine("OC.ExistsInGeneration('ID3') = " + 
      OC.ExistsInGeneration("ID3"));
}

private void CollectAll( )
{
    ////////////// BEGIN COLLECT //////////////
    GC.Collect( );
    GC.WaitForPendingFinalizers( );
    //////////////  END COLLECT  //////////////

    Console.WriteLine("\r\n--> Collect all weak references");
    Console.WriteLine("OC.TotalCacheSlots = " + OC.TotalCacheSlots( ));
    Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( ));
    Console.WriteLine("OC.ExistsInGeneration('ID1') = " + 
      OC.ExistsInGeneration("ID1"));
    Console.WriteLine("OC.ExistsInGeneration('ID2') = " + 
      OC.ExistsInGeneration("ID2"));
    Console.WriteLine("OC.ExistsInGeneration('ID3') = " + 
      OC.ExistsInGeneration("ID3"));
    Console.WriteLine("OC.ExistsInGeneration('ID5') = " + 
      OC.ExistsInGeneration("ID5"));
}

The output of this test code is shown here:

--> Add 5 weak references
OC.TotalCacheSlots = 5
OC.AliveObjsInCache = 5
OC.ExistsInGeneration('ID1') = 0

--> Collect all weak references
OC.TotalCacheSlots = 5
OC.AliveObjsInCache = 0

--> Add 5 weak references
OC.TotalCacheSlots = 5
OC.AliveObjsInCache = 5

--> Obtain ID1
SCOTemp.IDCode = 100
OC.ExistsInGeneration('ID1') = 0

--> Obtain ID1, ID3, ID5
OC.ExistsInGeneration('ID1') = 0

--> Collect all weak references
OC.TotalCacheSlots = 5
OC.AliveObjsInCache = 3
OC.ExistsInGeneration('ID1') = 1
SCO1.IDCode = 1000
SCO3.IDCode = 3000
SCO5.IDCode = 5000

--> Get ID2, which has been collected.  ID2 Exists == False
ID2 has now been re-created.  ID2 Exists == True
OC.AliveObjsInCache = 4
SCO2.IDCode = 2000

--> Collect all weak references
OC.TotalCacheSlots = 5
OC.AliveObjsInCache = 4
OC.ExistsInGeneration('ID1') = 2
OC.ExistsInGeneration('ID2') = 1
OC.ExistsInGeneration('ID3') = 2

--> Collect all weak references
OC.TotalCacheSlots = 5
OC.AliveObjsInCache = 0
OC.ExistsInGeneration('ID1') = -1
OC.ExistsInGeneration('ID2') = -1
OC.ExistsInGeneration('ID3') = -1
OC.ExistsInGeneration('ID5') = -1

Discussion

Caching involves storing frequently used objects in memory that are expensive to create and recreate for fast access. This technique is in contrast to recreating these objects through some time-consuming mechanism (e.g., from data in a database or from a file on disk) every time they are needed. By storing frequently used objects such as these—so that we do not have to create them nearly as much—we can further improve the performance of the application.

When deciding which types of items can be cached, you should look for objects that take a long time to create and/or initialize. For example, if an object's creation involves one or more calls to a database, to a file on disk, or to a network resource, it can be considered as a candidate for caching. In addition to selecting objects with long creation times, these objects should also be frequently used by the application.Selection depends on a combination of the frequency of use and the average time for which it is used in any given usage. Objects that remain in use for a long time when they are retrieved from the cache may work better in this cache than those that are frequently used but for only a very short period of time.

If you know that the number of cached objects will be equal to or less than 10, you can substitute a ListDictionary for the Hashtable. The ListDictionary is optimized for 10 items or fewer. If you are unsure of whether to pick a ListDictionary or a Hashtable, consider using a HybridDictionary object instead. A HybridDictionary object uses a ListDictionary when the number of items it contains is 10 or fewer. When the number of contained items exceeds 10, a Hashtable object is used. The switch from a ListDictionary to a Hashtable involves copying the elements from the ListDictionary to the Hashtable. This can cause a performance problem if this type of collection will usually contain more than 10 items. In addition, if the initial size of a ListDictionary is set above 10, a Hashtable is used by the HybridDictionary exclusively, again reducing the effectiveness of the HybridDictionary.

If you do not want to overwrite cached items having the same key as the object you are attempting to insert into the cache, the AddObj method must be modified. The code for the AddObj method could be modified to this:

public void AddObj(object key, SomeComplexObj item)
{
    WeakReference WR = new WeakReference(item, false);
    if (!cache.ContainsKey(key))
    {
        cache.Add(key, WR);
    }
    else
    {
        throw (new Exception("Attempt to insert duplicate keys."));
    }
}

We could also add a mechanism to calculate the cache-hit-ratio for this cache. The cache-hit-ratio is the ratio of hits—every time an existing object is requested from the Hashtable—to the total number of calls made to attempt a retrieval of an object. This can give us a good indication of how well our ObjCache is working. The code to add to this class to implement a cache-hit-ratio is shown highlighted here:

private float numberOfGets = 0;
private float numberOfHits = 0;

public float HitMissRatioPcnt( )
{
    if (numberOfGets == 0)
    {
        return (0);
    }
    else
    {
        return ((numberOfHits / numberOfGets) * 100);
    }
}

public SomeComplexObj GetObj(object key)
{
    ++numberOfGets;

    if (!cache.ContainsKey(key) || !IsObjAlive(key))
    {
        AddObj(key, new SomeComplexObj( ));
    }
    else
    {
        ++numberOfHits;
    }

    return ((SomeComplexObj)((WeakReference)cache[key]).Target);
}

The numberOfGets field tracks the number of calls made to the GetObj retrieval method. The numberOfHits field tracks the number of times that an object to be retrieved exists in the cache. The HitMissRatioPcnt method returns the numberOfHits divided by the numberOfGets as a percentage. The higher the percent, the better our cache is operating (100% is equal to a hit every time the GetObj method is called). A lower percentage indicates that this cache object is not working efficiently (0% is equal to a miss every time the GetObj method is called). A very low percentage indicates that the cache object may not be the correct solution to your problem or that you are not caching the correct object(s).

The WeakReference objects created for the ObjCache class do not track objects after they are finalized. This would add much more complexity than is needed by this class. Moreover, we would have the responsibility of dealing with resurrected objects that are in an undefined state. This is a dangerous path to follow.

Remember, a caching scheme adds complexity to your application. The most a caching scheme can do for your application is to enhance performance and possibly place less stress on memory resources. You should consider this when deciding whether to implement a caching scheme such as the one in this recipe.

See Also

To use the built-in ASP.NET cache object independently of a web application, see the following topics in MSDN:

  • "Caching Application Data"

  • "Adding Items to the Cache"

  • "Retrieving Values of Cached Items"

  • "Deleting Items from the Cache"

  • "Notifying an Application when an Item Is Deleted from the Cache"

  • "System.Web.Caching Namespace"

In addition, see the Datacache2 Sample under ".NET Samples—ASP.NET Caching" in MSDN; see the sample links to the Page Data Caching example in the ASP.NET QuickStart Tutorials.

Also see the "WeakReference Class" topic in the MSDN documentation.

    [ Team LiB ] Previous Section Next Section