Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the performance penalty of enabling stats on Guava Cache objects?

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 of CacheStats during the operation of the cache. Without this Cache.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.

like image 922
Patrick M Avatar asked Aug 07 '15 05:08

Patrick M


People also ask

Is Guava cache blocking?

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.

What is concurrency level in Guava 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.

Is Guava cache thread safe?

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.

How does Guava cache work?

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.


1 Answers

Lets take a look at the source code:

What happens when we call 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.

Then what happens when we construct a 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).

And when we use the 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.

And finally when we get the statistics?

When you call Cache.stats() the StatsCounters 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.

So in summary?

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.

like image 141
dimo414 Avatar answered Oct 13 '22 00:10

dimo414