views:

1751

answers:

4

Hi,

I've got a relatively expensive data-fetching operation that I want to cache the results of. This operation is called from const methods, roughly like this:

double AdjustData(double d, int key) const {
  double factor = LongRunningOperationToFetchFactor(key);
  return factor * d;
}

I'd like AdjustData to remain const, but I want to cache out the factor so I only fetch it the first time. At present I'm using a mutable map<int, double> to store the result (the map being from key to factor), but I'm thinking using a function-scoped static might be a better solution - this factor is only needed by this function, and is irrelevant to the rest of the class.

Does that seem like a good way to go? Are there any better options? What things might I think about, particularly with regard to thread-safety.

Thanks,

Dom

A: 

Unless I don't understand, it seems obvious to me that you want to make this a static:

double AdjustData(double d) const {
   static const double kAdjustFactor = LongRunningOperationToFetchFactor();
   return kAdjustFactor * d;
}

That way you only fetch the factor once.

Lyndsey Ferguson
Hi Lyndsey - thanks, that was what I was thinking. I've edited my question a bit to flesh out the actual problem. I can't use a static const in this case, so I guess I use a static map<int, double> and do some kind of locking around map inserts. Does that seem right?
Dominic Rodger
What you're talking about will work, but the solution has a somewhat *bad* smell :) (Taken from the Refactoring book). Is this something that the user initiates for many different 'keys' that you can run on a separate thread and display the result when it is done?
Lyndsey Ferguson
If you make it static, all objects of the class will share the value. Is this what you want?
Brian Neal
+1  A: 

You can use the singleton pattern(1) to with a class that performs the long-running operation and caches the result. This instance could then be used in const member functions of other classes. Consider mutual exclusion to protect inserts and extractions from the map data structure for thread safety. If multi-threaded performance is a huge issue, then you can flag keys as in progress to prevent multiple threads from calculating the same key simultaneously.

#include <cstdlib>
#include <iostream>
#include <map>

using namespace std;

class FactorMaker {
    map<int, double> cache;

    double longRunningFetch(int key)
    {
        const double factor = static_cast<double> (rand()) / RAND_MAX;
        cout << "calculating factor for key " << key << endl;
        // lock
        cache.insert(make_pair(key, factor));
        // unlock
        return factor;
    }

public:
    double getFactor(int key) {
        // lock
        map<int, double>::iterator it = cache.find(key);
        // unlock
        return (cache.end() == it) ? longRunningFetch(key) : it->second;
    }
};

FactorMaker & getFactorMaker()
{
    static FactorMaker instance;
    return instance;
}

class UsesFactors {
public:
    UsesFactors() {}

    void printFactor(int key) const
    {
        cout << getFactorMaker().getFactor(key) << endl;
    }
};

int main(int argc, char *argv[])
{
    const UsesFactors obj;

    for (int i = 0; i < 10; ++i)
        obj.printFactor(i);

    for (int i = 0; i < 10; ++i)
        obj.printFactor(i);

    return EXIT_SUCCESS;
}

(1) The singleton pattern can be grossly missed. So, please refrain from going crazy with it if you are seeing it for the first time.

Judge Maygarden
+5  A: 

I would wrap the implementation of LongRunningOperationToFetchFactor with something like this. I am using Boost scoped locks but you can so something similar with other locking frameworks.

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <map>

using namespace std;

static boost::mutex myMutex;
static map<int,double> results;

double CachedLongRunningOperationToFetchFactor( int key )
{

   {
       boost::mutex::scoped_lock lock(myMutex);

       map<int,double>::iterator iter = results.find(key);
       if ( iter != results.end() )
       {
          return (*iter).second;
       }
   }
   // not in the Cache calculate it
   result = LongRunningOperationToFetchFactor( key );
   {
       // we need to lock the map again
       boost::mutex::scoped_lock lock(myMutex);
       // it could be that another thread already calculated the result but
       // map assignment does not care.
       results[key] = result;
   }
   return result;
}

If this really is a long running operation then the cost of locking the Mutex should be minimal.

It was not quite clear from you question but if the function LongRunningOperationToFetchFactor is a member function of you class then you want the map the be mutable map in that same class. I single static mutex for access is still fast enough though.

James Dean
It may not be necessary to lock the long running operation, just find/insert calls on the map.
Judge Maygarden
I think you are right. Let me tune it up further.
James Dean
Creating the static map while not holding a lock is not guaranteed to be thread-safe, since you might double-construct and double-destruct the map if two threads call this function simultaneously the first time it is used. See http://blogs.msdn.com/oldnewthing/archive/2004/03/08/85901.aspx
bk1e
Also, what happens if a global object in another translation unit calls this function from its constructor? Your global mutex might not be constructed yet. Likewise, if a global object calls this function from its destructor, the mutex may have already been destructed.
bk1e
Just to be safe I moved the map to be file scope static so it should be created before the main() function is called. (I am assuming that this code function is not called before main.)
James Dean
+2  A: 

I would not make this cache a local static. The mutable map is the solution for caching results. Otherwise it will make your function useless, as different objects of your class will share the same cache, as the local static cache is the same for all objects. You can use the local static if the result does not depend on the object though. But then i would ask myself why the function is a non-static member of your object, if it does not need to access any state of it.

As you say it should be thread-safe - if different threads can call the member function on the same object, you probably want to use a mutex. boost::thread is a good library to use.

Johannes Schaub - litb
So the function is a non-static member of the object since it previously did depend on the state of the object (the mutable map). With a local static I can then make this function static.
Dominic Rodger
alright that is a different thing then :) just wanted to warn you about this if the LongRunningOperation is a member function that depends on your object :) one cannot be careful enough :)
Johannes Schaub - litb