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.
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(" --> 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(" --> 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(" --> Add 5 weak references"); Console.WriteLine("OC.TotalCacheSlots = " + OC.TotalCacheSlots( )); Console.WriteLine("OC.AliveObjsInCache = " + OC.AliveObjsInCache( )); CreateObjLongMethod( ); Create135( ); CollectAll( ); } private void CreateObjLongMethod( ) { Console.WriteLine(" --> 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(" --> 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(" --> 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(" --> 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(" --> 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(" --> 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
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.
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.