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: