-
Bug
-
Resolution: Fixed
-
P3
-
21.0.5
-
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
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
- relates to
-
JDK-8288723 Avoid redundant ConcurrentHashMap.get call in java.time
-
- Resolved
-
- links to
-
Commit(master) openjdk/jdk/9a60f445
-
Review(master) openjdk/jdk/22854