Статьи

Создание миллионов объектов с нулевым мусором

Как отмечалось в первом правиле оптимизации производительности, мусор является врагом быстрого кода. Мало того, что он может разрушить любую детерминированную производительность, используя службы сборщика мусора, но мы начинаем заполнять наши кэши ЦП мусором, что приведет к дорогостоящим потерям кэша для нашей программы.

Итак, можем ли мы использовать Java без создания мусора? Можно ли, например, в естественной Java решить эту проблему:

Создавайте 10-метровые объекты финансовых инструментов, сохраняйте их на карте, извлекайте их и выполняйте вычисления, используя каждый объект, не создавая никакого мусора.

Это если вы используете Chronicle ! Хроника предоставляет библиотеки, так что вы можете легко использовать кучу хранилища в виде файлов, отображаемых в памяти для ваших объектов. (Полный исходный код этой статьи см. Здесь .)

Давайте посмотрим реализацию решения для вышеуказанной проблемы.

Сначала давайте посмотрим, как вы можете сделать это в обычной Java, чтобы мы убедились, что понимаем проблему и что происходит, если мы используем стандартные библиотеки Java для нашей реализации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package zeroalloc;
 
import org.junit.Assert;
import org.junit.Test;
 
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
/**
 * Class to demonstrate zero garbage creation.
 * Run with -verbose:gc -Xmx4G
 */
public class CreateOnHeapTest {
    private static final int ITERATIONS = 10_000_000;
 
    @Test
    public void testOnHeapMap() {
        System.out.println("----- HASHMAP ------------------------");
        Map<Integer, BondVOImpl> map = new ConcurrentHashMap<>(ITERATIONS);
        long actualQuantity = 0;
        long expectedQuantity = 0;
        long time = System.currentTimeMillis();
 
        System.out.println("*** Entering critical section ***");
 
        for (int i = 0; i < ITERATIONS; i++) {
            BondVOImpl bondVo = new BondVOImpl();
            bondVo.setQuantity(i);
            map.put(Integer.valueOf(i), bondVo);
            expectedQuantity += i;
        }
 
 
        long putTime = System.currentTimeMillis() - time;
        time = System.currentTimeMillis();
        System.out.println("************* STARTING GET *********************");
        for (int i = 0; i < map.size(); i++) {
            actualQuantity += map.get(i).getQuantity();
        }
 
        System.out.println("*** Exiting critical section ***");
 
        System.out.println("Time for putting " + putTime);
        System.out.println("Time for getting " + (System.currentTimeMillis() - time));
 
        Assert.assertEquals(expectedQuantity, actualQuantity);
 
        printMemUsage();
    }
 
    public static void printMemUsage() {
        System.gc();
        System.gc();
        System.out.println("Memory(heap) used " + humanReadableByteCount(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), true));
    }
 
    public static String humanReadableByteCount(long bytes, boolean si) {
        int unit = si ? 1000 : 1024;
        if (bytes < unit) return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }
}

Это выход из программы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
*** Entering critical section ***
[GC (Allocation Failure)  98816K->92120K(125952K), 0.0317021 secs]
[Full GC (Ergonomics)  92120K->91917K(216576K), 0.2510530 secs]
[GC (Allocation Failure)  125197K->125430K(224256K), 0.0449051 secs]
[GC (Allocation Failure)  166390K->166686K(244224K), 0.0504341 secs]
[Full GC (Ergonomics)  166686K->165777K(387072K), 0.6243385 secs]
[GC (Allocation Failure)  226705K->226513K(388096K), 0.0785121 secs]
[GC (Allocation Failure)  293073K->293497K(392704K), 0.0825828 secs]
[Full GC (Ergonomics)  293497K->292649K(591872K), 1.2479519 secs]
[GC (Allocation Failure)  359209K->359433K(689664K), 0.0666344 secs]
[GC (Allocation Failure)  449033K->449417K(695296K), 0.1759746 secs]
[GC (Allocation Failure)  539017K->539385K(747008K), 0.1907760 secs]
[GC (Allocation Failure)  632569K->633009K(786944K), 0.2293778 secs]
[Full GC (Ergonomics)  633009K->631584K(1085952K), 2.1328028 secs]
[GC (Allocation Failure)  724768K->723368K(1146368K), 0.3092297 secs]
[GC (Allocation Failure)  827816K->825088K(1174016K), 0.3156138 secs]
[GC (Allocation Failure)  929536K->929952K(1207296K), 0.3891754 secs]
[GC (Allocation Failure)  1008800K->1009560K(1273856K), 0.4149915 secs]
[Full GC (Ergonomics)  1009560K->1007636K(1650688K), 3.4521240 secs]
[GC (Allocation Failure)  1086484K->1087425K(1671680K), 0.3884906 secs]
[GC (Allocation Failure)  1195969K->1196129K(1694208K), 0.2905121 secs]
[GC (Allocation Failure)  1304673K->1305257K(1739776K), 0.4291658 secs]
[GC (Allocation Failure)  1432745K->1433137K(1766912K), 0.4470582 secs]
[GC (Allocation Failure)  1560625K->1561697K(1832960K), 0.6003558 secs]
[Full GC (Ergonomics)  1561697K->1558537K(2343936K), 4.9359721 secs]
[GC (Allocation Failure)  1728009K->1730019K(2343936K), 0.7616385 secs]
[GC (Allocation Failure)  1899491K->1901139K(2413056K), 0.5187234 secs]
[Full GC (Ergonomics)  1901139K->1897477K(3119616K), 5.7177263 secs]
[GC (Allocation Failure)  2113029K->2114505K(3119616K), 0.6768888 secs]
[GC (Allocation Failure)  2330057K->2331441K(3171840K), 0.4812436 secs]
[Full GC (Ergonomics)  2331441K->2328578K(3530240K), 6.3054896 secs]
[GC (Allocation Failure)  2600962K->2488834K(3528704K), 0.1580837 secs]
*** Exiting critical section ***
Time for putting 32088
Time for getting 454
[GC (System.gc())  2537859K->2488834K(3547136K), 0.1599314 secs]
[Full GC (System.gc())  2488834K->2488485K(3547136K), 6.2759293 secs]
[GC (System.gc())  2488485K->2488485K(3559936K), 0.0060901 secs]
[Full GC (System.gc())  2488485K->2488485K(3559936K), 6.0975322 secs]
Memory(heap) used 2.6 GB

Два основных момента, которые выпрыгивают из этой проблемы, это, во-первых, количество и затраты на сборку мусора (очевидно, это можно настроить), и два — количество используемой кучи — 2,6 ГБ. Короче говоря, от этого никуда не деться, эта программа производит массу мусора.

Давайте попробуем точно так же, на этот раз используя ChronicleMap .

Это код для решения проблемы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package zeroalloc;
 
import net.openhft.chronicle.map.ChronicleMap;
import net.openhft.chronicle.map.ChronicleMapBuilder;
import net.openhft.lang.values.IntValue;
import org.junit.Assert;
import org.junit.Test;
 
import java.io.File;
import java.io.IOException;
 
/**
 * Class to demonstrate zero garbage creation.
 * Run with -verbose:gc
 * To run in JFR use these options for best results
 * -XX:+UnlockCommercialFeatures -XX:+FlightRecorder
 */
public class CreateChronicleTest {
    private static final int ITERATIONS = 10_000_000;
 
    @Test
    public void demoChronicleMap() throws IOException, InterruptedException {
        System.out.println("----- CHRONICLE MAP ------------------------");
        File file = new File("/tmp/chronicle-map-" + System.nanoTime() + ".map");
        file.deleteOnExit();
 
        ChronicleMapBuilder<IntValue, BondVOInterface> builder =
                ChronicleMapBuilder.of(IntValue.class, BondVOInterface.class)
                        .entries(ITERATIONS);
 
        try (ChronicleMap<IntValue, BondVOInterface> map =
                     builder.createPersistedTo(file)) {
            final BondVOInterface value = map.newValueInstance();
            final IntValue key = map.newKeyInstance();
            long actualQuantity = 0;
            long expectedQuantity = 0;
 
            long time = System.currentTimeMillis();
 
            System.out.println("*** Entering critical section ***");
 
            for (int i = 0; i < ITERATIONS; i++) {
                value.setQuantity(i);
                key.setValue(i);
                map.put(key, value);
                expectedQuantity += i;
            }
 
            long putTime = (System.currentTimeMillis()-time);
            time = System.currentTimeMillis();
 
            for (int i = 0; i < ITERATIONS; i++) {
                key.setValue(i);
                actualQuantity += map.getUsing(key, value).getQuantity();
            }
 
            System.out.println("*** Exiting critical section ***");
 
            System.out.println("Time for putting " + putTime);
            System.out.println("Time for getting " + (System.currentTimeMillis()-time));
 
            Assert.assertEquals(expectedQuantity, actualQuantity);
            printMemUsage();
 
        } finally {
            file.delete();
        }
    }
     
    public static void printMemUsage(){
        System.gc();
        System.gc();
        System.out.println("Memory(heap) used " + humanReadableByteCount(Runtime.getRuntime().totalMemory()
           - Runtime.getRuntime().freeMemory(), true));
    }
 
    public static String humanReadableByteCount(long bytes, boolean si) {
        int unit = si ? 1000 : 1024;
        if (bytes < unit) return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }
}

Это вывод из программы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
[GC (Allocation Failure)  33280K->6595K(125952K), 0.0072065 secs]
[GC (Allocation Failure)  39875K->12177K(125952K), 0.0106678 secs]
[GC (Allocation Failure)  45457K->15289K(125952K), 0.0068434 secs]
[GC (Allocation Failure)  48569K->18357K(159232K), 0.0098287 secs]
[GC (Allocation Failure)  84917K->21008K(159232K), 0.0156393 secs]
*** Entering critical section ***
*** Exiting critical section ***
Time for putting 8554
Time for getting 4351
[GC (System.gc())  36921K->21516K(230400K), 0.0331916 secs]
[Full GC (System.gc())  21516K->15209K(230400K), 0.0630483 secs]
[GC (System.gc())  15209K->15209K(230912K), 0.0006491 secs]
[Full GC (System.gc())  15209K->15209K(230912K), 0.0234045 secs]
Memory(heap) used 18.2 MB

Основной момент здесь, очевидно, заключается в том, что в критическом разделе не было сборщиков мусора и что вся программа использовала только 18 МБ кучи. Нам удалось создать программу, которая обычно производила бы гигабайты мусора без какого-либо мусора вообще.

Записка о сроках

ChronicleMap явно не является заменой ConcurrentHashMap, они имеют совершенно другое применение, и в этом посте выходит за рамки этого поста слишком углубляться в эту линию обсуждения. Но основные различия в функциональности заключаются в том, что ChronicleMap сохраняется и может использоваться многими JVM. (ChronicleMap также имеет возможность репликации tcp.) Тем не менее, интересно быстро сравнить время, если не что иное, как убедиться, что мы находимся в одном и том же парке. ChronicleMap был быстрее для установки, 8,5 с по сравнению с 32 с. Но большую часть времени в ConcurrentHashMap было проведено в GC, и это может быть в некоторой степени отключено. ConcurrentHashMap был быстрее для получения, 0,5 с по сравнению с 4,3 с. Тем не менее, на других запусках я видел ConcurrentHashMap, принимающий более 7 с из-за GC, который произошел в этом разделе. Несмотря на то, что ChronicleMap выполняет значительно больше работы, отсутствие производимого мусора фактически делает время сопоставимым с ConcurrentHashMap.

Перезапуск программы

Где ChronicleMap действительно вступает в свои права — это перезапуск. Допустим, ваша программа не работает и вам нужно пересчитать те же вычисления, что и ранее. В случае ConcurrentHashMap нам пришлось бы заново заполнить карту точно так же, как мы делали ранее. С ChronicleMap, поскольку карта постоянна, достаточно просто навести карту на существующий файл и перезапустить расчет, чтобы получить общее количество.

Резюме

ConcurrentHashMap ChronicleMap
gc паузы Многие Никто
Время обновления 32s 8s
читает позволяет gc 7s 4s
не читает gc 0.5с 4s
размер кучи 2,6 ГБ 18MB
упорство нет да
быстрый перезапуск нет да