In the previous post I investigated using single map with struct keys verses using map of maps of maps in Go language. Here I want to run the same test for Java.

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Fork(3)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class MapMapMapBenchmark {

    @Benchmark
    public Map<Integer, Map<Integer, Map<Integer, String>>> mapMapMap() {
        var firsts = new HashMap<Integer, Map<Integer, Map<Integer, String>>>();
        test(((first, second, third, value) -> {
            var seconds = firsts.computeIfAbsent(first, unused -> new HashMap<>());
            var thirds = seconds.computeIfAbsent(second, unused -> new HashMap<>());
            thirds.put(third, value);
        }));
        return firsts;
    }

    @Benchmark
    public Map<Integer, Map<Integer, Map<Integer, String>>> mapMapMapExact() {
        var firsts = HashMap.<Integer, Map<Integer, Map<Integer, String>>>newHashMap(FIRSTS);
        test(((first, second, third, value) -> {
            var seconds = firsts.computeIfAbsent(first, unused -> HashMap.newHashMap(SECONDS));
            var thirds = seconds.computeIfAbsent(second, unused -> HashMap.newHashMap(THIRDS));
            thirds.put(third, value);
        }));
        return firsts;
    }

    record IntKey(int first, int second, int third) {
    }

    @Benchmark
    public Map<IntKey, String> singleMap() {
        var map = new HashMap<IntKey, String>();
        test((first, second, third, value) -> map.put(new IntKey(first, second, third), value));
        return map;
    }


    @Benchmark
    public Map<IntKey, String> singleMapExact() {
        var map = HashMap.<IntKey, String>newHashMap(FIRSTS * SECONDS * THIRDS);
        test((first, second, third, value) -> map.put(new IntKey(first, second, third), value));
        return map;
    }

    interface Tester {
        void accept(int first, int second, int third, String value);
    }

    //not static, not final to prevent constant folding (see JMHSample_10_ConstantFold)
    private int FIRSTS = 100;
    private int SECONDS = 10;
    private int THIRDS = 10;

    private void test(Tester tester) {
        for (int first = 0; first < FIRSTS; first++) {
            for (int second = 0; second < SECONDS; second++) {
                for (int third = 0; third < THIRDS; third++) {
                    tester.accept(first, second, third, first + "-" + second + "-" + third);
                }
            }
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(".*" + MapMapMapBenchmark.class.getSimpleName() + ".*")
                .addProfiler(GCProfiler.class)
                .build();

        new Runner(opt).run();
    }
}

Running it I got these results.

Benchmark                                     Mode  Cnt        Score     Error   Units
Benchmark.mapMapMap                           avgt   15      379.447 ±  15.983   us/op
Benchmark.mapMapMap:·gc.alloc.rate            avgt   15     2461.668 ± 105.561  MB/sec
Benchmark.mapMapMap:·gc.alloc.rate.norm       avgt   15   978128.156 ±   0.007    B/op
Benchmark.mapMapMap:·gc.count                 avgt   15      124.000            counts
Benchmark.mapMapMap:·gc.time                  avgt   15       79.000                ms
Benchmark.mapMapMapExact                      avgt   15      292.492 ±  30.147   us/op
Benchmark.mapMapMapExact:·gc.alloc.rate       avgt   15     4263.455 ± 413.234  MB/sec
Benchmark.mapMapMapExact:·gc.alloc.rate.norm  avgt   15  1297112.120 ±   0.012    B/op
Benchmark.mapMapMapExact:·gc.count            avgt   15      202.000            counts
Benchmark.mapMapMapExact:·gc.time             avgt   15      146.000                ms
Benchmark.singleMap                           avgt   15      162.902 ±   0.913   us/op
Benchmark.singleMap:·gc.alloc.rate            avgt   15     6856.225 ±  38.460  MB/sec
Benchmark.singleMap:·gc.alloc.rate.norm       avgt   15  1171248.067 ±   0.002    B/op
Benchmark.singleMap:·gc.count                 avgt   15      222.000            counts
Benchmark.singleMap:·gc.time                  avgt   15      151.000                ms
Benchmark.singleMapExact                      avgt   15      129.910 ±   1.519   us/op
Benchmark.singleMapExact:·gc.alloc.rate       avgt   15     8116.308 ±  92.953  MB/sec
Benchmark.singleMapExact:·gc.alloc.rate.norm  avgt   15  1105616.054 ±   0.002    B/op
Benchmark.singleMapExact:·gc.count            avgt   15      260.000            counts
Benchmark.singleMapExact:·gc.time             avgt   15      190.000                ms

The map of maps of maps case without upfront map creation is obviously the slowest one. However its allocations per operations are the smallest too. But the performance difference is so huge, that it is pretty obvious that single map with record keys should be preferred. This may be retested with non-primitive record fields, however is some CPU-bound tasks the advantage of on way of dealing with maps over the other is obvious.