home *** CD-ROM | disk | FTP | other *** search
- /*
- C A C H E C L A S S
- for Borland C++ 4.5
- Version 1
-
- Andrew Sterian
- asterian@umich.edu
- July 11, 1996
-
- Introduction
- ============
-
- The Cache class implements a simple caching mechanism with "least
- recently used" (LRU) replacement. It is useful for cases in which you
- have some memory to spare and you wish to speed up access to frequently
- used resources, of which not all will fit into available memory. Thus,
- you know you have room in memory for more than 1 resource, but you don't
- have room to store all possible resources in memory, so you want to
- store some of them. The Cache class helps you to manage the storage in
- memory of some subset of all possible resources and therefore speed up
- your resource accesses.
-
- For an example of where a cache may be useful, consider a word
- processing program that uses many fonts. In painting the editing window,
- each time a new font must be displayed on the screen the existing font
- has to be deleted and a new font created for use. Font creation may take
- a long time, so to speed up the process it would be nice to pre-create
- the fonts and store them in memory, ready for use. The problem is that
- fonts can also take up a lot of memory, and creating and storing all
- possible fonts in memory is unacceptable.
-
- If the user is only working with a small subset of all fonts, we need
- only store that subset in memory. But which fonts are in the subset? And
- what if the subset of fonts changes over time? The Cache class manages
- this situation by maintaining a list of the last N resources (fonts)
- used (N is the "cache depth", passed to the constructor of the class).
- When the N+1th resource needs to be created, the Cache class will remove
- the least recently used (LRU) resource of the existing N resources in
- memory to make room for the new entry. Thus, at any time, there are a
- maximum of N resources taking up memory, and they are precisely the last
- N resources that were needed by the application.
-
- The reason for keeping the last N resources in memory is due to a
- property of many programs known as "temporal locality of reference". In
- plain English, this means that a program tends to use resources that it
- has already (recently) used. For example, memory caches in
- microprocessors exploit the fact that much of an application's code
- consists of loops that are localized to a fairly small area of memory.
- The CPU will therefore likely need to fetch instruction words from
- addresses that have already been recently accessed (code in the loop).
-
- Data, too, often exhibits locality of reference. Consider the simple
- task of updating a variable. First, its value is retrieved (resulting in
- a memory access), the value is modified, then written back. But the
- write-back occurs to the memory address that was just accessed, so
- keeping this address in the cache immediately saves one reference. If
- this variable will be used more than once in the code that follows (as
- is often the case) then we save a reference each time the variable is
- accessed.
-
- If, however, the application does NOT exhibit good temporal locality of
- reference, then a cache may be of little or no use. Comparing the number
- of cache "hits" to the total number of accesses is usually a good
- benchmark of the cache's utility. CPU program data caches often exhibit
- hit rates exceeding 90%, a great savings.
-
- Design Requirements
- ===================
-
- To use the Cache class you need to provide two things:
-
- 1. A "key class". This key class will be used as a label for the
- resources that you wish to load. For example, if the resources we
- are working with are fonts, the "key label" may be the alphabetic
- name of the font, such as "MS Sans Serif 10".
-
- The key class needs to provide:
- - a meaningful equality (==) operator
- - either a member function with prototype
- unsigned HashValue(void) const
-
- or you need to provide a global function:
- unsigned HashValue(KeyClass& key)
-
- This function needs to return an unsigned quantity that is a
- deterministic function of the key object. This unsigned
- quantity need not be unique for different key objects,
- although this may improve performance. Computing a "good"
- hash value for a generic object has been called a study in
- art more than science, although there are some general
- principles to follow (Reingold and Hansen, "Data Structures
- in Pascal", Little, Brown & Co. 1986):
-
- * the hash value should depend upon every bit of the key
- * the hash value should break up naturally occurring
- clusters of elements. That is, if two keys are somehow
- logically related (for example, different point sizes of
- the same typeface) then their hash values should not be
- similar.
- * the hash value should be computed very quickly. Remember,
- the idea of a cache is to save time.
-
- The April 1996 issue of "Dr. Dobb's Journal" contains a brief
- article on this topic and even provides some code for a good,
- generalized hashing function.
-
- - a default constructor (just to keep the class libraries
- happy, this constructor need not do anything meaningful)
-
- Note that your "key class" need not be a class at all, it can be
- some built-in type, like 'int', for which you write a global
- HashValue() function.
-
- 2. A "resource class". This class encapsulates the resource that we are
- trying to manage.
-
- The resource class must:
- - provide a constructor of the form:
- ResourceClass(const KeyClass& key)
-
- That is, given a key belonging to the key class described
- above, the resource class constructor should be able to find
- and acquire the resource corresponding to this key.
-
- - acquire the resource in the constructor
-
- - release the resource in the destructor
-
- The last two points are important. The Cache class implementation
- does not assume the existence of any Acquire() or Release()
- functions in the resource class to explicitly construct and free the
- resource. It is assumed that the resource management is performed in
- the constructor and destructor of the resource class.
-
- This behavior is considered to be a "good thing"(tm). The features
- of the C++ language ensure that no destructor is called for a class
- that has not been completely constructed. To this end, it is further
- suggested that the resource allocation in the constructor be
- performed in the instantiation part of the constructor, not the
- initialization. For example, this is preferred:
-
- class ResourceClass { // PREFERRED
- public:
- ResourceClass(const KeyClass& key)
- : data(GetResource(key)) { }
- ~ResourceClass() { FreeResource(data); }
- MyData* data;
- };
-
- over the following:
-
- class ResourceClass { // ***NOT RECOMMENDED***
- public:
- ResourceClass(const KeyClass& key)
- { data = GetResource(key); }
- ~ResourceClass() { FreeResource(data); }
- MyData* data;
- };
-
- which can lead to incorrect behavior if resource acquisition fails.
-
- Usage
- =====
-
- Once you have designed your key class and resource class, you can
- instantiate a cache for this resource as follows:
-
- typedef Cache<KeyClass, ResourceClass> MyCache;
- MyCache *theCache = new MyCache(depth);
-
- where 'depth' indicates the cache depth, or the maximum number of
- resources that you want cached at any one time. Obviously, the larger
- the depth the better the performance but the more memory will be used.
-
- The Cache constructor takes an optional second parameter which is the
- size of the internal hash table to use. This table is not visible
- outside the Cache class but its size has an impact on performance.
- Larger hash tables lead to faster searches through the cache, but will
- obviously use more memory. Do note, however, that it is highly
- recommended that the hash table size be a prime number, or else
- performance can severely degrade in certain circumstances. The default
- hash table size is 109. For some reason, the default hash table size
- provided by Borland is 111 which is not prime (in fact, it has the small
- prime 3 as a factor making it one of the poorest odd table sizes you
- could use).
-
- The main interface to the cache is nearly transparent. Whenever you wish
- to acquire a resource, create a key for the resource and simply call the
- Load() member function:
-
- KeyClass lKey(parameters);
- ResourceClass* lRes = theCache->Load(lKey);
-
- If the resource is already in the cache, then Load() will return a
- pointer to the existing resource. Otherwise, Load() will construct a new
- ResourceClass object (passing the key to the constructor), add the
- object to the cache, and pass back the pointer to the object. In either
- case, you don't have to worry about what is or is not in the cache, the
- Cache class does it all for you. Just use Load() to always create
- resources.
-
- If the resource to be loaded is not in the cache and the cache is full,
- then the least recently used item in the cache will be discarded to make
- room for the new resource. The destructor for ResourceClass, therefore,
- should release the resource since it will be invoked at this time.
-
- There are other member functions in the Cache class that may be of
- interest. These functions are provided for support of debugging or
- statistics gathering and need not be used in normal operation.
-
- - unsigned long Hits(void)
- returns the number of cache "hits" encountered since the cache
- was created or since the last Flush() operation. A cache "hit"
- occurs when the Load() function is called and the desired
- resource is already in the cache.
-
- - unsigned long Misses(void)
- returns the number of cache "misses", but is otherwise
- identical to the Hits() function. A cache "miss" occurs when
- Load() is called but the desired resource is NOT in the cache
- and must be created. The "hit rate" can be simply computed as
- 100.0*Hits()/(Hits()+Misses()).
-
- - void Flush(void)
- removes all items from the cache (calling all ResourceClass
- destructors to physically release the resources) and resets the
- cache-hit and cache-miss counters.
-
- - void Depth(void)
- returns the current cache depth. The cache depth is specified
- in the constructor but may be increased (see below).
-
- - void Depth(unsigned newDepth)
- This form of the Depth() function increases the cache depth to
- the size specified by 'newDepth'. If 'newDepth' is smaller than
- the current depth, then nothing happens.
-
- - unsigned CachedItems(void)
- returns the number of items in the cache
-
- - ResourceClass *LRUItemPeek(void)
- returns a pointer to the resource element that is in the cache
- and was the least recently used. That is, this points to the
- resource that will be the next to be removed from the cache
- (unless it loses its least-recently-used status by being
- accessed through Load()). If the cache is empty, this function
- returns NULL.
-
- - void LRUItemPop(void)
- removes the least-recently-used resource from the cache,
- calling the destructor for the resource class element so the
- physical resource is released. If the cache is empty, this
- function does nothing.
-
- Example
- =======
-
- Here is a real example for how to use the Cache class. The running
- example of fonts as resources that we've been using is cumbersome since
- fonts have so many parameters associated with them, so we will use
- something easier, a Windows bitmap resource. This may be useful for a
- program that uses very many bitmaps and does not wish to store them all
- in memory. Yet, the bitmaps exhibit good temporal locality of reference
- so that a bitmap that was used recently is likely to be used again soon.
-
- A bitmap is loaded using the Windows LoadBitmap() function, which
- requires us to specify an instance handle (to identify the executable
- file containing the bitmap resource), as well as the name of the bitmap
- resource as a string (we're ignoring the MAKEINTRESOURCE possibility).
- The key class must include these two values:
-
- class BitmapKey {
- public:
- BitmapKey(HINSTANCE hInstance, string bitmapName)
- : inst(hInstance), name(bitmapName) { }
-
- BitmapKey() {} // default constructor to keep libraries happy
-
- operator ==(const BitmapKey& obj) {
- return inst==obj.inst && name==obj.name;
- }
- unsigned HashValue(void) const {
- // We conveniently use the hash() function of the string
- // class. The hash function given here has no claim to
- // being "good".
- return (inst ^ name.hash());
- }
- HINSTANCE inst;
- string name;
- };
-
- Now we must define a class that encapsulates the actual bitmap. It must
- also define how to load the bitmap (given a BitmapKey element) and
- release the resource.
-
- class BitmapClass {
- public:
- // Load physical resource in constructor
- BitmapClass(const BitmapKey& key)
- : handle(LoadBitmap(key.inst, key.name.c_str()))
- { }
-
- // Release resource in destructor
- ~BitmapClass() { DeleteObject(handle); }
-
- HBITMAP handle;
- }
-
- Done! To use the above classes in a program, we may do something like
- the following:
-
- Cache<BitmapKey, BitmapClass> bmCache(10); // Cache up to 10 bitmaps
-
- while (1) {
- string s;
-
- GetBitmapName(s); // Ask user for a valid bitmap name
-
- BitmapClass* bm = bmCache.Load(BitmapKey(hInstance, s));
-
- DrawBitmap(bm->handle); // Draw the bitmap on the screen
- }
-
- Of course, this program has no temporal locality of reference since the
- user can ask for any bitmap in any order, thus this program is unlikely
- to benefit from using a cache. But hopefully it demonstrates the basic
- principles of using the Cache class.
-
- Future Work
- ===========
-
- This code was written in one evening. Consequently, there is lots of
- room for improvement. Off the top of my head, I can think of the
- following possible refinements or optimizations:
-
- - An allocator specialized for the CacheItem class can be used to
- provide faster mark-and-release style memory management for the
- class, making insertions and removals of cache items from the
- hash table more efficient.
-
- - A mechanism for turning off the cache altogether could be
- implemented. This is often useful for debugging or for comparing
- with-cache and without-cache performance without having to modify
- the code (although a depth of 1 would be a good approximation).
- Come to think of it, I can't remember why I didn't want to allow
- the depth to be reduced. Otherwise, shrinking the depth to 1
- would basically be equivalent to turning the cache off.
-
- - The DDJ article mentioned above makes a case for using a closed
- hash table with linear rehashing instead of the traditional
- implementation of an open hash table (linked list for each hash
- slot). This would require redefining the TDictionary class to use
- a customized (closed) hash table instead of the supplied Borland
- implementation. If the number of items to be cached is known a
- priori and a table size can be created that is at least twice
- this number, then this may provide some performance improvement.
- All this work is certainly not worth it for me, though.
-
- - LRU is generally a good replacement policy but improvements on
- this basic approach exist. For example, shadow directories
- (Pomerene et. al. 1984).
-
- - One source of inefficiency in this code is the call to Detach()
- in the markRecentlyUsed() routine. This call essentially performs
- a linear search through the LRU list to find the item before
- removing it from its current position and adding it back to the
- tail. For large cache depths, this linear search can be costly. A
- more intelligent data structure arrangement could probably reduce
- this O(N) search into an O(log N) or even constant-time search.
- My use of the Cache class usually uses small depths (10 or 20) so
- a linear search is not a big deal for me.
-
- - In the dictionary, for a given hash slot any new items are added
- to either the head or tail of the linked list at that slot (I
- haven't dived into Borland's code to figure out which one). If we
- are to exploit temporal locality of reference to its fullest, we
- should ensure that the most recently used cache item comes at the
- head. This is because we predict that it will be used again soon,
- so a search through this list would stop as close to the head as
- possible (for minimum searching time). If the hash table size is
- greater than the cache depth, then each linked list will likely
- have at most 2 or 3 elements so this issue is probably no big
- deal.
-
- Administrivia
- =============
-
- This code is CommunityWare. You may use it at no cost. If you do use it,
- however, I ask that you consider taking a useful C++ class that you have
- written and releasing it to the public for free.
-
- Please feel free to drop me some e-mail if you do find this class
- useful, I'd love to hear from you! If you have questions or problems
- regarding this code, then please write me, but I am afraid that I cannot
- guarantee any kind of support or aid in getting this software to work.
- This software is provided "as is".
-
- If you modify this code, please keep a record of modifications
- (including your name, so that I am not held responsible for your changes
- :-)
-
- Andrew Sterian
- asterian@umich.edu
- */
-
- #ifndef _CACHE_H_
- #define _CACHE_H_
-
- #include <classlib/assoc.h> // Defines TAssociation classes
- #include <classlib/dict.h> // Defines TDictionary classes
- #include <classlib/dlistimp.h> // Defines TDoubleList classes
-
- template <class K, class T>
- class Cache {
- public:
- // Each entry in the dictionary (a CacheItem) consists of a key and
- // a pointer to a resource class based on that key.
- typedef TDIAssociation<K,T> CacheItem;
-
- Cache(unsigned depth, unsigned hashSize = 109)
- : mDict(hashSize), mDepth(depth), mHits(0), mMisses(0) {
- CHECK(depth >= 1);
- mDict.OwnsElements(1);
- }
- ~Cache() { mDict.Flush(); } // Ensure all resources are freed
-
- void Flush(void) { mDict.Flush(); mLRUList.Flush(); mHits=mMisses=0; }
- unsigned Depth(void) const { return mDepth; }
- void Depth(unsigned newDepth) {
- if (newDepth > mDepth) mDepth=newDepth;
- }
-
- // This is the main accessor of the Cache class. It should be used to
- // create all resources.
- T* Load(const K& key);
-
- unsigned long Hits(void) const { return mHits; }
- unsigned long Misses(void) const { return mMisses; }
- unsigned CachedItems(void) const { return mLRUList.GetItemsInContainer(); }
-
- // For debugging, these functions can be useful.
- T* LRUItemPeek(void) const {
- return CachedItems() ? CONST_CAST(T *, mLRUList.PeekHead()->Value())
- : 0;
- }
- void LRUItemPop(void) {
- if (CachedItems()) discardLRUItem();
- }
-
- protected:
- TIDictionaryAsHashTable<CacheItem> mDict;
- TIDoubleListImp<CacheItem> mLRUList;
- unsigned mDepth;
- unsigned long mHits;
- unsigned long mMisses;
-
- void markRecentlyUsed(CacheItem *lItemPtr);
- void addCacheItem(CacheItem *lItemPtr);
- void discardLRUItem(void);
- };
-
- template <class K, class T>
- void Cache<K,T>::markRecentlyUsed(CacheItem *lItemPtr)
- {
- // Find the object in the LRU list, wherever it may be, and remove it
- mLRUList.Detach(lItemPtr);
-
- // Now add it at the tail. Most recently used objects are at the tail
- // of this list, so that LRU objects float to the head.
- mLRUList.AddAtTail(lItemPtr);
- }
-
- template <class K, class T>
- void Cache<K,T>::addCacheItem(CacheItem *lItemPtr)
- {
- // Add the pointer to the item to the LRU list
- mLRUList.AddAtTail(lItemPtr);
-
- // Add it to the Dictionary too
- mDict.Add(lItemPtr);
- }
-
- template <class K, class T>
- void Cache<K,T>::discardLRUItem(void)
- {
- CHECK(! mLRUList.IsEmpty());
-
- // Get the item from the head of the LRU list
- CacheItem *lHead = mLRUList.PeekHead();
- mLRUList.DetachAtHead();
-
- // Now remove it from the dictionary
- mDict.Detach(lHead);
- }
-
- template <class K, class T>
- T* Cache<K,T>::Load(const K& key)
- {
- CacheItem lLook(key, 0), *lItem;
-
- // First look for the key in the dictionary. If found, it's a cache
- // hit and we just return the associated resource. Otherwise we
- // must construct the resource and add this new entry into the cache.
- if ((lItem = mDict.Find(&lLook)) != 0) {
- mHits++;
- markRecentlyUsed(lItem);
- } else {
- mMisses++;
-
- // Get a new object by constructing it as T(key)
- lItem = new CacheItem(key, new T(key));
-
- // Remove the LRU entry from the dictionary if we've exceeded our depth
- if (mLRUList.GetItemsInContainer() >= mDepth) {
- discardLRUItem();
- }
-
- // Add new item to dictionary
- addCacheItem(lItem);
- }
- return CONST_CAST(T *, lItem->Value());
- }
- #endif // _CACHE_H_
-
-