classdef CachedData < handle
% Cache data rather than re-generate every time
%
% Sometimes, it can be somewhat tricky to implement code in a way that
% prevents re-reading or re-calculating the exact some thing with only
% a short time in between. Don't despair! Here is the class you'll
% love. Just use its methods to store and retrieve data.
%
% Entries are stored by string. Internally, it uses a structure, so you
% may use genvarname to generate valid names to use for caching.
%
%
% CachedData Properties:
%
% maxsize - Maximum permitted cache size in bytes
%
% CachedData Methods:
%
% get_entry - Retrieve entry
% has_entry - Check whether entry exists
% set_entry - Set new entry and, if needed, remove old
% clear - Completely clear the cache
% cachesize - Get current cache-size in bytes
% evaluate - Evaluate expression and cache result, or read
% result from cache
% $Id$
properties
% maximum permitted cache size in bytes
%
% default 100 MiB
maxsize = 100*2^20;
end
properties (GetAccess = private, SetAccess = private)
cache;
end
methods
function self = CachedData()
self = self@handle();
self.cache = struct();
end
function data = get_entry(self, s)
% Retrieve entry from cache
%
% FORMAT
%
% data = c.get_entry(s)
data = self.cache.(s);
end
function b = has_entry(self, s)
% check whether entry exists in cache
%
% FORMAT
%
% b = c.has_entry(s)
b = isfield(self.cache, s);
end
function set_entry(self, s, d)
% sets cache entry and removes old entry if needed
%
% Set a cache entry and, if the size is exceeded, remove the
% oldest entry.
%
% FORMAT
%
% c.set_entry(s, d)
%
% IN
%
% s string string by which it is stored
% d any data to be stored
logtext(atmlab('OUT'), 'Setting cache entry: %s (new size: ', s);
self.cache.(s) = d;
fprintf(atmlab('OUT'), '%d entries, %s)\n', length(fieldnames(self.cache)), nbytes2string(self.cachesize()));
if self.toolarge()
self.del_old_entry();
end
end
function clear(self)
% clear the cache completely
self.cache = struct();
end
function c = cachesize(self)
% get size in bytes
dummy = self.cache; %#ok
X = whos('dummy');
c = X.bytes;
end
function varargout = evaluate(self, no, func, varargin)
% Execute func(arg1, arg2, ...) if output not already cached
%
% This method calculates a key from all its arguments. If no
% value is stored in association with this key, it executes
% func(arg1, arg2, ...) and stores the results with the key,
% then returns the results. If there is a value already stored,
% it returns the value(s). The first argument is a number,
% it represents the number of output arguments taken from the
% expression.
%
% FORMAT
%
% out = cd.evaluate(no, @func, arg1, arg2, ...)
%
% IN
%
% no number of output arguments. Needed because
% there may be a difference between y = foo(...)
% and [y, z] = foo(...)
%
% func function handle to function to be evaluated
% arg1 1st argument passed to function
% ...
% argN nth argument passed to function
% 'EXTRA' literal string 'EXTRA'. The remaining arguments
% WILL be used to calculate the hash, but WON'T be passed on
% to others. Use this e.g. when calculating obj.meth(args)
% for different objects (pass obj.name or so).
%
% OUT
%
% output is as for func(arg1, ..., argN).
%
% EXAMPLE
%
% If you have an expensive function 'y = ackermann(m, n)',
% then you can cache as follows:
%
% >> c = CachedData(); % create cache object
% >> y = c.evaluate(1, @ackermann, 4, 4) % come back a few million years later
% >> y = c.evaluate(1, @ackermann, 4, 4) % get result immediately
%
% generate a unique (?) string
nm = genvarname(DataHash([{no, func}, varargin]));
extra = 0;
args = varargin;
% check if any equal to 'EXTRA'
for i = 1:length(varargin)
if isequal(varargin{i}, 'EXTRA')
extra = i;
args = varargin(1:extra-1);
break;
end
end
if self.has_entry(nm)
logtext(atmlab('OUT'), 'getting %s([%d arguments]) from cache (%s)\n', ...
func2str(func), length(args), nm);
varargout = self.get_entry(nm);
else
logtext(atmlab('OUT'), 'executing, then caching %s([%d arguments])\n', ...
func2str(func), length(args));
[varargout{1:no}] = func(args{:});
self.set_entry(nm, varargout);
end
end
end
methods (Access = private)
function del_old_entry(self)
% Remove oldest entry in cache.
%
% FORMAT
%
% c.del_old_entry()
flds = fieldnames(self.cache);
logtext(atmlab('OUT'), 'Pruning cache entry %s\n', flds{1});
self.cache = rmfield(self.cache, flds{1});
logtext(atmlab('OUT'), 'New size: %d entries, %d bytes\n', length(fieldnames(self.cache)), self.cachesize());
end
function b = toolarge(self)
% whether or not to remove old entry
%
% Check whether the current size exceeds the total size.
b = self.cachesize() > self.maxsize;
end
end
end