在 Guava Cache 对象上启用统计信息的性能损失是多少?

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

显然,正确答案是 'benchmark it and find out',但本着互联网的精神,我希望有人能为我完成这项工作。

我非常喜欢 Guava 的网络服务缓存库。然而,他们的文档在这一点上相当模糊。

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)

来自 CacheBuilder.recordStats().

的 JavaDocs

我很好奇性能损失的严重程度是否被任何人记录、基准测试或模拟。我认为它应该很小,每次操作的数量级为纳秒。缓存操作本身已经同步 - 读取不会锁定或阻塞,但写入会获取锁定 - 因此不需要额外的锁定或并发来修改统计信息。这应该将其限制为每次缓存访问一些额外的增量操作。

它的另一面可能是调用 Cache.stats() 时的一些惩罚。我计划通过 Codahale MetricsRegistry 将统计数据公开到 Graphite 服务器上进行持久记录。最终效果是会定期检索统计信息,因此如果检索时出现任何阻塞行为,那可能会很糟糕。

我们来看看 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 如果它不能对它跟踪的每个统计信息使用 LongAdder)。

那我们构造一个Cache会怎么样呢?

对于相同 StatsCounter 类型的标准 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。其他 Cache 实现可以根据需要选择使用 SimpleStatsCounter,或者提供自己的行为(例如无操作实现)。

而当我们使用 Cache?

每次对 LocalCache 的调用都会影响其中一项统计信息,并调用相关的 StatsCounter.record*() 方法,这反过来会导致支持 LongAddable 上的原子增量或添加。 LongAdder 被记录为比 AtomicLong 快得多,所以就像你说的那样,这应该很难被注意到。尽管在无操作 StatsRecorder 的情况下,JIT 可以完全优化掉 record*() 调用,随着时间的推移,这 可能 会很明显。但决定不在此基础上跟踪统计数据肯定是 premature optimization

最后我们什么时候得到统计数据?

当您在新的 StatsCounter 中调用 Cache.stats() the StatsCounters for the Cache and all its Segments are aggregated together 并将结果返回给您时。这意味着将有最少的阻塞;每个字段只需要读取一次,并且没有外部同步或锁定。这确实意味着技术上存在竞争条件(可以在聚合中途访问段),但实际上这无关紧要。

总而言之?

在您有兴趣监视的任何 Cache 上使用 CacheBuilder.recordStats() 并尽可能频繁地调用 Cache.stats() 是有益的,您应该感到自在。内存开销大致恒定,速度开销可以忽略不计(并且比您可能实施的任何类似监控更快),Cache.stats().

的争用开销也是如此

显然,专用线程除了在循环中调用 Cache.stats() 什么都不做会引起一些争用,但那是愚蠢的。任何类型的定期访问都不会引起注意。