Five code performance improvement skills in Java, up to nearly 10 times

Posted by lightningrod66 on Sun, 26 Dec 2021 09:04:04 +0100

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.

  1. Pre allocate the size of HashMap to improve the performance by 1 / 4.
  2. Optimize the key of HashMap, and the performance difference is 9.5 times.
  3. Do not use enum Values () traversal, and Spring has been so optimized.
  4. Using Enum instead of String constant, the performance is 1.5 times higher.
  5. 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

subscribe

You can search through wechat Program ape Alan Or visit Program ape Alan blog read.
this paper Github.com/niumoo/JavaNotes It has been included. There are many knowledge points and series articles. Welcome Star.

Topics: Java Optimize