/* * Copyright 2015 Ben Manes. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.benmanes.caffeine.cache; import static java.util.function.Function.identity; import java.io.PrintStream; import java.math.RoundingMode; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.github.jamm.MemoryMeter; import org.github.jamm.MemoryMeter.Guess; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.math.LongMath; import com.jakewharton.fliptables.FlipTable; /** * A non-JMH benchmark to compare the memory overhead of different cache implementations. Note that * the measurements estimate based on the current JVM configuration, e.g. 64-bit with compressed * references if the benchmark is executed with a heap under 32GB. This can means that object * padding may or may not have a visible effect. * <p> * This benchmark requires a JavaAgent to evaluate the object sizes and can be executed using * <tt>gradle -q memoryOverhead</tt>. * * @author ben.manes@gmail.com (Ben Manes) */ public final class MemoryBenchmark { // The number of entries added to minimize skew due to non-entry factors static final int FUZZY_SIZE = 25_000; // The maximum size, which is larger than the fuzzy factor due to Guava's early eviction static final int MAXIMUM_SIZE = 2 * FUZZY_SIZE; // The pre-computed entries to store into the cache when computing the per-entry overhead static final Map<Integer, Integer> workingSet = IntStream.range(0, FUZZY_SIZE) .boxed().collect(Collectors.toMap(identity(), i -> -i)); final MemoryMeter meter = new MemoryMeter() .withGuessing(Guess.FALLBACK_BEST) .ignoreKnownSingletons(); final PrintStream out = System.out; public void run() throws Exception { if (!MemoryMeter.hasInstrumentation()) { out.println("WARNING: Java agent not installed - guessing instead"); } out.println(); unbounded(); maximumSize(); maximumSize_expireAfterAccess(); maximumSize_expireAfterWrite(); maximumSize_refreshAfterWrite(); maximumWeight(); expireAfterAccess(); expireAfterWrite(); expireAfterAccess_expireAfterWrite(); weakKeys(); weakValues(); weakKeys_weakValues(); weakKeys_softValues(); softValues(); } private Caffeine<Object, Object> builder() { // Avoid counting ForkJoinPool in estimates return Caffeine.newBuilder().executor(Runnable::run); } private void unbounded() { Cache<Integer, Integer> caffeine = builder().build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder().build(); compare("Unbounded", caffeine, guava); } private void maximumSize() { Cache<Integer, Integer> caffeine = builder().maximumSize(MAXIMUM_SIZE).build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .maximumSize(MAXIMUM_SIZE).build(); compare("Maximum Size", caffeine, guava); } private void maximumWeight() { Cache<Integer, Integer> caffeine = builder() .maximumWeight(MAXIMUM_SIZE).weigher((k, v) -> 1).build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .maximumWeight(MAXIMUM_SIZE).weigher((k, v) -> 1).build(); compare("Maximum Weight", caffeine, guava); } private void maximumSize_expireAfterAccess() { Cache<Integer, Integer> caffeine = builder() .expireAfterAccess(1, TimeUnit.MINUTES) .maximumSize(MAXIMUM_SIZE) .build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.MINUTES) .maximumSize(MAXIMUM_SIZE) .build(); compare("Maximum Size & Expire after Access", caffeine, guava); } private void maximumSize_expireAfterWrite() { Cache<Integer, Integer> caffeine = builder() .expireAfterWrite(1, TimeUnit.MINUTES) .maximumSize(MAXIMUM_SIZE) .build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES) .maximumSize(MAXIMUM_SIZE) .build(); compare("Maximum Size & Expire after Write", caffeine, guava); } private void maximumSize_refreshAfterWrite() { Cache<Integer, Integer> caffeine = builder() .refreshAfterWrite(1, TimeUnit.MINUTES) .maximumSize(MAXIMUM_SIZE) .build(k -> k); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .refreshAfterWrite(1, TimeUnit.MINUTES) .maximumSize(MAXIMUM_SIZE) .build(new CacheLoader<Integer, Integer>() { @Override public Integer load(Integer key) { return key; } }); compare("Maximum Size & Refresh after Write", caffeine, guava); } private void expireAfterAccess() { Cache<Integer, Integer> caffeine = builder() .expireAfterAccess(1, TimeUnit.MINUTES).build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.MINUTES).build(); compare("Expire after Access", caffeine, guava); } private void expireAfterWrite() { Cache<Integer, Integer> caffeine = builder() .expireAfterWrite(1, TimeUnit.MINUTES).build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES).build(); compare("Expire after Write", caffeine, guava); } private void expireAfterAccess_expireAfterWrite() { Cache<Integer, Integer> caffeine = builder() .expireAfterAccess(1, TimeUnit.MINUTES) .expireAfterWrite(1, TimeUnit.MINUTES) .build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.MINUTES) .expireAfterWrite(1, TimeUnit.MINUTES) .build(); compare("Expire after Access & after Write", caffeine, guava); } private void weakKeys() { Cache<Integer, Integer> caffeine = builder().weakKeys().build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .weakKeys().build(); compare("Weak Keys", caffeine, guava); } private void weakValues() { Cache<Integer, Integer> caffeine = builder().weakValues().build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .weakValues().build(); compare("Weak Values", caffeine, guava); } private void weakKeys_weakValues() { Cache<Integer, Integer> caffeine = builder().weakKeys().weakValues().build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .weakKeys().weakValues().build(); compare("Weak Keys & Weak Values", caffeine, guava); } private void weakKeys_softValues() { Cache<Integer, Integer> caffeine = builder().weakKeys().softValues().build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .weakKeys().softValues().build(); compare("Weak Keys & Soft Values", caffeine, guava); } private void softValues() { Cache<Integer, Integer> caffeine = builder().softValues().build(); com.google.common.cache.Cache<Integer, Integer> guava = CacheBuilder.newBuilder() .softValues().build(); compare("Soft Values", caffeine, guava); } private void compare(String label, Cache<Integer, Integer> caffeine, com.google.common.cache.Cache<Integer, Integer> guava) { caffeine.cleanUp(); guava.cleanUp(); int leftPadded = Math.max((36 - label.length()) / 2 - 1, 1); out.printf(" %2$-" + leftPadded + "s %s%n", label, " "); String result = FlipTable.of(new String[] { "Cache", "Baseline", "Per Entry" },new String[][] { evaluate("Caffeine", caffeine.asMap()), evaluate("Guava", guava.asMap()) }); out.println(result); } private String[] evaluate(String label, Map<Integer, Integer> map) { long base = meter.measureDeep(map); map.putAll(workingSet); long populated = meter.measureDeep(map); long entryOverhead = 2 * FUZZY_SIZE * meter.measureDeep(workingSet.keySet().iterator().next()); long perEntry = LongMath.divide(populated - entryOverhead - base, FUZZY_SIZE, RoundingMode.HALF_EVEN); perEntry += ((perEntry & 1) == 0) ? 0 : 1; long aligned = ((perEntry % 8) == 0) ? perEntry : ((1 + perEntry / 8) * 8); return new String[] { label, String.format("%,d bytes", base), String.format("%,d bytes (%,d aligned)", perEntry, aligned) }; } public static void main(String[] args) throws Exception { new MemoryBenchmark().run(); } }