Caching method results is an easy way of improving performance when you're dealing with slow functions, such as:
- fetching database results;
- complex mathematical calculations;
- poorly designed algorithms that you're too lazy to improve :-)
Wouldn't it be nice if the programming language you're using allowed to cache method results automatically?
As far as I know only Perl and Python allow this (maybe I'll cover them in future posts). What about Java? Are we doomed to implement caching over and over?
AspectJ allows you to intercept method calls (along with many other useful things), so what about creating an aspect which would intercept that really slow method, check ed if the method had been called before with the same arguments and returned the previous returned value without calling the function?
So, to test the concept I had to create a small program that called a slow function over and over with the same arguments.
The following program calculates 1000! a thousand times
import java.math.BigInteger;
public class Factorial {
public static void main(String[] args) {
BigInteger n = BigInteger.ONE;
Factorial f= new Factorial();
long begin = System.currentTimeMillis();
for(int i = 0; i != 1000; ++i )
n = f.fact(1000);
long end = System.currentTimeMillis();
System.out.println(n);
System.out.println("Took "
+ (end - begin) / 1000.0
+ " seconds");
}
public BigInteger fact(int value) {
if(value == 0 || value == 1)
return new BigInteger("1");
BigInteger n = new BigInteger(
Integer.toString(value));
for(int i = value - 1; i != 1; --i)
n = n.multiply(new BigInteger(
Integer.toString(i)));
return n;
}
}
On my machine (PowerPc 7447, 1.2 ghz), the loop took about 16,6 seconds to run.
Now, I'll present two simple aspects:
Generic caching code :
- intercept method calls
- generate an hash key for the method arguments concatenating the string representation of all the arguments
- On the first call, store the method return value on the hash table and return it
- On subsequent calls return the cached value
import java.util.HashMap;
public abstract aspect Cache pertarget(method()){
HashMap<String, Object> cache =
new HashMap<String, Object>();
String hashKey(Object[] arguments) {
String key = "";
for (Object object : arguments)
key += object;
return key;
}
Object around(): method() {
String key = hashKey(thisJoinPoint.getArgs());
if (cache.containsKey(key)) {
return cache.get(key);
} else {
Object value = proceed();
cache.put(key,value);
return value;
}
}
abstract pointcut method();
}
Now, there's another aspect to intercept the factorial calculation
import java.math.BigInteger;
public aspect CacheFactorial extends Cache {
pointcut method() :
call(BigInteger Factorial.fact(int));
}
Now the same exact program only takes 0,11 seconds! That's a really good improvement with almost no effort thanks to the power of AspectJ.
Now, before using this code, please note that:
- the cache will grow uncontrolled over time (unless you're using the same argument, but in that case you don't need a cache at all), exhausting all available memory.
- beware of collisions with the key generating code. There's no point in getting the wrong answer really fast.
- The cache doesn't expire. If you're caching a database query, you'll probably want to refresh the cache now and then.
Do not use this code as it is. I'll improve it in future posts. Stay tuned!
There are thousands of articles out there about the subject.
There's More Than One Way To Do It! :-)