The article keeps updating, and can pay attention to the official account. Program ape Alan Or visit Unread code blog.
this paper Github.com/niumoo/JavaNotes Already included, welcome Star.
This article introduces several tips for performance optimization in Java development. Although extreme code optimization is not necessary in most cases, as a technical developer, we still want to pursue smaller, faster and stronger code. If one day you find that the running speed of the program is not satisfactory, you may think of this article.
Tip: we should not optimize for optimization, which sometimes increases the complexity of the code.
The code in this article is tested in the following environments.
- JMH version: 1.33 (Java benchmark framework)
- VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724
Through the test of this article, we will find the performance differences of the following operations.
- Pre allocate the size of HashMap to improve the performance by 1 / 4.
- Optimize the key of HashMap, and the performance difference is 9.5 times.
- Do not use enum Values () traversal, and Spring has been so optimized.
- Using Enum instead of String constant, the performance is 1.5 times higher.
- Using a higher version of JDK, there is a 2-5x performance difference in basic operations.
The current article belongs to Java performance analysis and optimization Series of articles, click to view all articles.
The tests in the current article use JMH benchmark. Related articles: Java code performance test using JMH.
Preallocate the size of the HashMap
HashMap is one of the most commonly used collections in Java. Most operations are very fast, but HashMap is very slow and difficult to optimize automatically when adjusting its capacity. Therefore, we should give its capacity as much as possible before defining a HashMap. The load factor should be considered when giving the size value. The default load factor of HashMap is 0.75, that is, the size value to be set should be divided by 0.75.
Related articles: Analysis and interpretation of HashMap source code
The following is a benchmark test using JMH to test the efficiency of inserting 14 elements into the HashMap with an initial capacity of 16 and 32, respectively.
/** * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3,time = 3) @Measurement(iterations = 5,time = 3) public class HashMapSize { @Param({"14"}) int keys; @Param({"16", "32"}) int size; @Benchmark public HashMap<Integer, Integer> getHashMap() { HashMap<Integer, Integer> map = new HashMap<>(size); for (int i = 0; i < keys; i++) { map.put(i, i); } return map; } }
The initial capacity of HashMap is 16, which is responsible for the factor of 0.75, that is, a maximum of 12 elements are inserted, and then the capacity needs to be expanded. Therefore, the capacity needs to be expanded once in the process of inserting 14 elements. However, if 32 capacity is given during HashMap initialization, it can carry up to 32 * 0.75 = 24 elements, so the capacity does not need to be expanded when inserting 14 elements.
# JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark (keys) (size) Mode Cnt Score Error Units HashMapSize.getHashMap 14 16 thrpt 25 4825825.152 ± 323910.557 ops/s HashMapSize.getHashMap 14 32 thrpt 25 6556184.664 ± 711657.679 ops/s
It can be seen that in this test, the HashMap with an initial capacity of 32 can operate 26% more times per second than the HashMap with an initial capacity of 16, with a performance difference of 1 / 4.
Optimize the key of HashMap
If the key value of HashMap needs to use multiple String strings, taking the String as a class attribute and then using the instance of this class as the key will be more efficient than using String splicing.
The following tests the efficiency difference between using two strings as keys and using two strings as attribute references of MutablePair class, and then using MutablePair object as key.
/** * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) public class HashMapKey { private int size = 1024; private Map<String, Object> stringMap; private Map<Pair, Object> pairMap; private String[] prefixes; private String[] suffixes; @Setup(Level.Trial) public void setup() { prefixes = new String[size]; suffixes = new String[size]; stringMap = new HashMap<>(); pairMap = new HashMap<>(); for (int i = 0; i < size; ++i) { prefixes[i] = UUID.randomUUID().toString(); suffixes[i] = UUID.randomUUID().toString(); stringMap.put(prefixes[i] + ";" + suffixes[i], i); // use new String to avoid reference equality speeding up the equals calls pairMap.put(new MutablePair(prefixes[i], suffixes[i]), i); } } @Benchmark @OperationsPerInvocation(1024) public void stringKey(Blackhole bh) { for (int i = 0; i < prefixes.length; i++) { bh.consume(stringMap.get(prefixes[i] + ";" + suffixes[i])); } } @Benchmark @OperationsPerInvocation(1024) public void pairMap(Blackhole bh) { for (int i = 0; i < prefixes.length; i++) { bh.consume(pairMap.get(new MutablePair(prefixes[i], suffixes[i]))); } } }
Test results:
# JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark Mode Cnt Score Error Units HashMapKey.pairMap thrpt 25 89295035.436 ± 6498403.173 ops/s HashMapKey.stringKey thrpt 25 9410641.728 ± 389850.653 ops/s
It can be found that the performance of using object reference as key is 9.5 times that of using String splicing as key.
Do not use enum Values() traversal
We usually use enum Values () performs enumeration class traversal, but each call will allocate an array of enumeration class values for operation. Here, it can be cached to reduce the time and space consumption of each memory allocation.
/** * Enumeration class traversal test * * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class EnumIteration { enum FourteenEnum { a,b,c,d,e,f,g,h,i,j,k,l,m,n; static final FourteenEnum[] VALUES; static { VALUES = values(); } } @Benchmark public void valuesEnum(Blackhole bh) { for (FourteenEnum value : FourteenEnum.values()) { bh.consume(value.ordinal()); } } @Benchmark public void enumSetEnum(Blackhole bh) { for (FourteenEnum value : EnumSet.allOf(FourteenEnum.class)) { bh.consume(value.ordinal()); } } @Benchmark public void cacheEnums(Blackhole bh) { for (FourteenEnum value : FourteenEnum.VALUES) { bh.consume(value.ordinal()); } } }
Operation results
# JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark Mode Cnt Score Error Units EnumIteration.cacheEnums thrpt 25 15623401.567 ± 2274962.772 ops/s EnumIteration.enumSetEnum thrpt 25 8597188.662 ± 610632.249 ops/s EnumIteration.valuesEnum thrpt 25 14713941.570 ± 728955.826 ops/s
Obviously, the traversal speed after using cache is the fastest, and the traversal efficiency using EnumSet is the lowest. It is easy to understand that the traversal efficiency of array is greater than that of hash table.
You may feel that you can use values() caching here and enum directly The efficiency difference of values() is very small. In fact, it is very different in some scenarios with high call frequency. Enum. Is used in the Spring framework values() traverses the HTTP status code enumeration class during each response, which causes unnecessary performance overhead when the number of requests is large. Later, values() cache optimization is carried out.
Here is This submission Screenshot of:
Use Enum instead of String constant
Using Enum enumeration class instead of String constant has obvious advantages. Enumeration class is forced to verify without error. At the same time, it is more efficient to use enumeration class. Even as the key value of the Map, although the speed of HashMap is very fast, the speed of using EnumMap can be faster.
Tip: don't optimize for optimization, which will increase the complexity of the code.
The following test uses Enum as the key and String as the key in map Performance difference under get operation.
/** * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) public class EnumMapBenchmark { enum AnEnum { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; } /** Number of key s to find */ private static int size = 10000; /** Random number seed */ private static int seed = 99; @State(Scope.Benchmark) public static class EnumMapState { private EnumMap<AnEnum, String> map; private AnEnum[] values; @Setup(Level.Trial) public void setup() { map = new EnumMap<>(AnEnum.class); values = new AnEnum[size]; AnEnum[] enumValues = AnEnum.values(); SplittableRandom random = new SplittableRandom(seed); for (int i = 0; i < size; i++) { int nextInt = random.nextInt(0, Integer.MAX_VALUE); values[i] = enumValues[nextInt % enumValues.length]; } for (AnEnum value : enumValues) { map.put(value, UUID.randomUUID().toString()); } } } @State(Scope.Benchmark) public static class HashMapState{ private HashMap<String, String> map; private String[] values; @Setup(Level.Trial) public void setup() { map = new HashMap<>(); values = new String[size]; AnEnum[] enumValues = AnEnum.values(); int pos = 0; SplittableRandom random = new SplittableRandom(seed); for (int i = 0; i < size; i++) { int nextInt = random.nextInt(0, Integer.MAX_VALUE); values[i] = enumValues[nextInt % enumValues.length].toString(); } for (AnEnum value : enumValues) { map.put(value.toString(), UUID.randomUUID().toString()); } } } @Benchmark public void enumMap(EnumMapState state, Blackhole bh) { for (AnEnum value : state.values) { bh.consume(state.map.get(value)); } } @Benchmark public void hashMap(HashMapState state, Blackhole bh) { for (String value : state.values) { bh.consume(state.map.get(value)); } } }
Operation results:
# JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark Mode Cnt Score Error Units EnumMapBenchmark.enumMap thrpt 25 22159.232 ± 1268.800 ops/s EnumMapBenchmark.hashMap thrpt 25 14528.555 ± 1323.610 ops/s
Obviously, the performance of using Enum as key is 1.5 times higher than that of using String as key. However, it is still necessary to consider whether to use EnumMap and EnumSet according to the actual situation.
Use a higher version of JDK
The String class should be the most frequently used class in Java, but the String implementation in Java 8 takes up more space and has lower performance than the higher version JDK.
Next, test the performance overhead of String to bytes and bytes to String in Java 8 and Java 11.
/** * @author https://www.wdbyte.com * @date 2021/12/23 */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) public class StringInJdk { @Param({"10000"}) private int size; private String[] stringArray; private List<byte[]> byteList; @Setup(Level.Trial) public void setup() { byteList = new ArrayList<>(size); stringArray = new String[size]; for (int i = 0; i < size; i++) { String uuid = UUID.randomUUID().toString(); stringArray[i] = uuid; byteList.add(uuid.getBytes(StandardCharsets.UTF_8)); } } @Benchmark public void byteToString(Blackhole bh) { for (byte[] bytes : byteList) { bh.consume(new String(bytes, StandardCharsets.UTF_8)); } } @Benchmark public void stringToByte(Blackhole bh) { for (String s : stringArray) { bh.consume(s.getBytes(StandardCharsets.UTF_8)); } } }
Test results:
# JMH version: 1.33 # VM version: JDK 1.8.0_151, Java HotSpot(TM) 64-Bit Server VM, 25.151-b12 Benchmark (size) Mode Cnt Score Error Units StringInJdk.byteToString 10000 thrpt 25 2396.713 ± 133.500 ops/s StringInJdk.stringToByte 10000 thrpt 25 1745.060 ± 16.945 ops/s # JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark (size) Mode Cnt Score Error Units StringInJdk.byteToString 10000 thrpt 25 5711.954 ± 41.865 ops/s StringInJdk.stringToByte 10000 thrpt 25 8595.895 ± 704.004 ops/s
It can be seen that the performance of Java 17 is about 2.5 times that of Java 8 in the operation of bytes to String, while the performance of Java 17 is 5 times that of Java 8 in the operation of String to bytes. The operation of String is very basic and can be seen everywhere. It can be seen that the advantages of high version are very obvious.
As always, the code examples in the current article are stored in github.com/niumoo/JavaNotes.
reference resources
- https://richardstartin.github.io/posts/5-java-mundane-performance-tricks
- https://github.com/spring-projects/spring-framework/issues/26842
- https://github.com/spring-projects/spring-framework/commit/7f1062159ee9926d5abed7cadc2b36b6b7fc242e
subscribe
You can search through wechat Program ape Alan Or visit Program ape Alan blog .
this paper Github.com/niumoo/JavaNotes Already included, welcome Star.