Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8345668

ZoneOffset.ofTotalSeconds performance regression

XMLWordPrintable

    • b05
    • generic
    • generic

        A DESCRIPTION OF THE PROBLEM :
        After "8288723: Avoid redundant ConcurrentHashMap.get call in java.time" there is a significant impact on the performance of "ZoneOffset.ofTotalSeconds".

        REGRESSION : Last worked in version 17.0.13

        STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
        See the source code below.

        EXPECTED VERSUS ACTUAL BEHAVIOR :
        EXPECTED -
        Performance to be same or better.
        ACTUAL -
        On my machine, the test case produces the following result:
        single JDK21: 8.035184 ns/call
        single JDK17: 2.463525 ns/call
        multi JDK21: 1.032913 ns/call
        multi JDK17: 0.314105 ns/call

        ---------- BEGIN SOURCE ----------
        import java.time.ZoneOffset;
        import java.util.concurrent.ConcurrentHashMap;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
        import java.util.concurrent.TimeUnit;

        public class ZoneOffsetPerf {

            public static void main(String[] args) throws InterruptedException {
                ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(0);
                int iterations = 100_000_000;

                singleThreadPerformance(iterations, zoneOffset);
                multiThreadPerformance(iterations, zoneOffset);
            }

            private static void singleThreadPerformance(int iterations, ZoneOffset zoneOffset) {
                long start = System.nanoTime();
                for (int i = 0; i < iterations; i++) {
                    if (ZoneOffset.ofTotalSeconds(0) != zoneOffset) {
                        throw new RuntimeException(); // Don't optimize away
                    }
                }
                long end = System.nanoTime();
                double nanosPerCall = (end - start) / (double)iterations;
                System.out.println("single JDK21: %05f ns/call".formatted(nanosPerCall));

                start = System.nanoTime();
                for (int i = 0; i < iterations; i++) {
                    if (RegressionWorkaround.zoneOffsetOfTotalSeconds(0) != zoneOffset) {
                        throw new RuntimeException(); // Don't optimize away
                    }
                }
                end = System.nanoTime();
                nanosPerCall = (end - start) / (double) iterations;
                System.out.println("single JDK17: %05f ns/call".formatted(nanosPerCall));
            }

            private static void multiThreadPerformance(int iterations, ZoneOffset zoneOffset) throws InterruptedException {
                int nThreads = 16;
                try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) {
                    long start = System.nanoTime();
                    for(int i = 0; i < nThreads; i++) {
                        executorService.submit(() -> {
                            for (int j = 0; j < iterations; j++) {
                                if (ZoneOffset.ofTotalSeconds(0) != zoneOffset) {
                                    throw new RuntimeException(); // Don't optimize away
                                }
                            }
                        });
                    }
                    executorService.shutdown();
                    executorService.awaitTermination(1, TimeUnit.MINUTES);

                    long end = System.nanoTime();
                    double nanosPerCall = (end - start) / ((double) iterations * nThreads);
                    System.out.println("multi JDK21: %05f ns/call".formatted(nanosPerCall));
                }

                try (ExecutorService executorService = Executors.newFixedThreadPool(nThreads)) {
                    long start = System.nanoTime();
                    for(int i = 0; i < nThreads; i++) {
                        executorService.submit(() -> {
                            for (int j = 0; j < iterations; j++) {
                                if (RegressionWorkaround.zoneOffsetOfTotalSeconds(0) != zoneOffset) {
                                    throw new RuntimeException(); // Don't optimize away
                                }
                            }
                        });
                    }
                    executorService.shutdown();
                    executorService.awaitTermination(1, TimeUnit.MINUTES);

                    long end = System.nanoTime();
                    double nanosPerCall = (end - start) / ((double) iterations * nThreads);
                    System.out.println("multi JDK17: %05f ns/call".formatted(nanosPerCall));
                }
            }

            public static final class RegressionWorkaround {
                public static final ConcurrentHashMap<Integer, ZoneOffset> SECONDS_CACHE = new ConcurrentHashMap<>();

                private RegressionWorkaround() {}

                public static ZoneOffset zoneOffsetOfTotalSeconds(int totalSeconds) {
                    if (totalSeconds % (15 * 60) == 0) {
                        Integer totalSecs = totalSeconds;
                        ZoneOffset result = SECONDS_CACHE.get(totalSecs);
                        if (result == null) {
                            result = ZoneOffset.ofTotalSeconds(totalSeconds);
                            SECONDS_CACHE.putIfAbsent(totalSecs, result);
                        }
                        return result;
                    } else {
                        return ZoneOffset.ofTotalSeconds(totalSeconds);
                    }
                }
            }
        }
        ---------- END SOURCE ----------

        CUSTOMER SUBMITTED WORKAROUND :
        Adding a top-layer cache that mimics the jdk17 behavior.

        public static final class RegressionWorkaround {
            public static final ConcurrentHashMap<Integer, ZoneOffset> SECONDS_CACHE = new ConcurrentHashMap<>();

            private RegressionWorkaround() {}

            public static ZoneOffset zoneOffsetOfTotalSeconds(int totalSeconds) {
                if (totalSeconds % (15 * 60) == 0) {
                    Integer totalSecs = totalSeconds;
                    ZoneOffset result = SECONDS_CACHE.get(totalSecs);
                    if (result == null) {
                        result = ZoneOffset.ofTotalSeconds(totalSeconds);
                        SECONDS_CACHE.putIfAbsent(totalSecs, result);
                    }
                    return result;
                } else {
                    return ZoneOffset.ofTotalSeconds(totalSeconds);
                }
            }
        }

        FREQUENCY : always


              naoto Naoto Sato
              webbuggrp Webbug Group
              Votes:
              1 Vote for this issue
              Watchers:
              10 Start watching this issue

                Created:
                Updated:
                Resolved: