Obviously, the correct answer is 'benchmark it and find out', but in the spirit of the internet, I'm hoping someone will have done the work for me.
I really like Guava's cache libraries for web services. Their docs are fairly vague on this point, however.
recordStats
public CacheBuilder<K,V> recordStats()
Enable the accumulation ofCacheStats
during the operation of the cache. Without thisCache.stats()
will return zero for all statistics. Note that recording stats requires bookkeeping to be performed with each operation, and thus imposes a performance penalty on cache operation.Since:
12.0 (previously, stats collection was automatic)
From JavaDocs for CacheBuilder.recordStats()
.
I'm curious if the severity of the performance penalty is documented, benchmarked or ball-parked by anyone. I'm thinking it should be pretty minor, on the order of nanoseconds per operation. The cache operations themselves are already synchronized - reads don't lock or block, but writes do acquire locks - so no additional locking or concurrency should be required to modify the stats. That should limit it to a few additional increment operations per cache access.
The other side of it is perhaps some penalty when Cache.stats()
is called. I'm planning on exposing the stats to persistent recording through Codahale MetricsRegistry and onto a Graphite server. The net effect is that the stats will be retrieved periodically, so if there's any blocking behavior on retrieval, that could be bad.
A non-blocking cache implementation. Guava java library has an interface LoadingCache that has methods related with cache. The library also provides a CacheBuilder whose constructor needs a CacheLoader that has different methods to load values into the cache.
Guava's cache is built on top of Java 5's ConcurrentHashMap with a default concurrency level of 4. This setting is because that hash table is segmented into multiple smaller tables, so more segments allows for higher concurrency at a cost of a larger memory footprint.
Cache entries are manually added using get(Object, Callable) or put(Object, Object) , and are stored in the cache until either evicted or manually invalidated. Implementations of this interface are expected to be thread-safe, and can be safely accessed by multiple concurrent threads.
Guava provides a very powerful memory based caching mechanism by an interface LoadingCache<K,V>. Values are automatically loaded in the cache and it provides many utility methods useful for caching needs.
Lets take a look at the source code:
CacheBuilder.recordStats()
?CacheBuilder
defines a no-op StatsCounter
implementation NULL_STATS_COUNTER
and this is what is used by default. If you call .recordStats()
this is replaced with SimpleStatsCounter
which has six LongAddable
fields (which is usually a LongAdder
but falls back to an AtomicLong
if it can't use LongAdder
) for each of the statistics it tracks.
Cache
?For a standard LocalCache
(which is what you get from CacheBuilder.build()
or CacheBuilder.build(CacheLoader)
), it constructs an instance of the desired StatsCounter
during construction. Each Segment
of the Cache
similarly gets its own instance of the same StatsCounter
type. Other Cache
implementations can choose to use a SimpleStatsCounter
if they desire, or provide their own behavior (e.g. a no-op implementation).
Cache
?Every call into LocalCache
that would impact one of the statistics calls the relevant StatsCounter.record*()
methods, which in turn causes an atomic increment or addition on the backing LongAddable
. LongAdder
is documented to be significantly faster than AtomicLong
, so like you say this should be hardly noticeable. Though in the case of the no-op StatsRecorder
the JIT could optimize away the record*()
calls entirely, which could maybe be noticeable over time. But deciding not to track statistics on that basis would surely be premature optimization.
When you call Cache.stats()
the StatsCounter
s for the Cache
and all its Segments
are aggregated together in a new StatsCounter
and the result returned to you. This means there will be minimal blocking; each field only needs to be read once, and there's no external synchronizing or locking. This does mean there's technically a race condition (a segment could be accessed midway through the aggregation) but in practice that's irrelevant.
You should feel comfortable using CacheBuilder.recordStats()
on any Cache
you're interested in monitoring, and calling Cache.stats()
as frequently as is beneficial. The memory overhead is roughly constant, the speed overhead is negligible (and faster than any similar monitoring you could likely implement), as is the contention overhead of Cache.stats()
.
Obviously a dedicated thread doing nothing but calling Cache.stats()
in a loop will cause some contention, but that would be silly. Any sort of periodic access will go unnoticed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With