Статьи

Закулисные секреты Jsoup V: советы и рекомендации по оптимизации

Мы сделали все правильно, теперь пришло время делать вещи быстрее. Мы должны помнить предупреждение Дональда Кнута : «Мы должны забыть о малой эффективности, скажем, в 97% случаев: преждевременная оптимизация — корень всего зла».

По словам Джонатана Хедли, он использует YourKit Java Profiler для измерения использования памяти и определения горячей точки производительности. Использование статистических результатов такого рода инструментов имеет решающее значение для успеха оптимизаций, так как вы не будете тратить время на то, чтобы просто задуматься и сделать бесполезные настройки, которые не улучшат производительность, а также сделают ваш код излишне сложным и сложным в обслуживании. Джонатан также говорил об этом в «Колофоне» .

Мы перечислим некоторые советы и приемы, используемые в Jsoup, они случайным образом упорядочены в настоящее время, будут реорганизованы в будущем.

Секреты Jsoup

1. Заполнение для отступа

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// memoised padding up to 21, from "", " ", "  " to "                   "
static final String[] padding = {......};
 
public static String padding(int width) {
    if (width < 0)
        throw new IllegalArgumentException("width must be > 0");
 
    if (width < padding.length)
        return padding[width];
    char[] out = new char[width];
    for (int i = 0; i < width; i++)
        out[i] = ' ';
    return String.valueOf(out);
}
 
protected void indent(Appendable accum, int depth, Document.OutputSettings out) throws IOException {
    accum.append('\n').append(StringUtil.padding(depth * out.indentAmount()));
}

Довольно умный, верно? Он поддерживает кэш различной длины, который покрывал бы 80% случаев — что, я полагаю, будет основано на опыте и статистике автора.

2. Есть класс или нет?

Element#hasClass был помечен как чувствительный к производительности , например, мы хотим проверить, есть ли у <div class="logged-in env-production intent-mouse"> production классов, разделить класс по пробелам на массив, после чего цикл и поиск будут работать , но в глубоком обходе это было бы неэффективно. Jsoup сначала вводит здесь « Ранний выход» , сравнивая длину с именем целевого класса, чтобы избежать ненужного сканирования и поиска, что также будет полезно. Затем он использует указатель для обнаружения пробелов и выполняет regionMatches — честно говоря, я впервые узнал метод String#regionMatches & # 55907; & # 56357; & # 56837; & # 56837;

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
public boolean hasClass(String className) {
    final String classAttr = attributes().getIgnoreCase("class");
    final int len = classAttr.length();
    final int wantLen = className.length();
 
    if (len == 0 || len < wantLen) {
        return false;
    }
 
    // if both lengths are equal, only need compare the className with the attribute
    if (len == wantLen) {
        return className.equalsIgnoreCase(classAttr);
    }
 
    // otherwise, scan for whitespace and compare regions (with no string or arraylist allocations)
    boolean inClass = false;
    int start = 0;
    for (int i = 0; i < len; i++) {
        if (Character.isWhitespace(classAttr.charAt(i))) {
            if (inClass) {
                // white space ends a class name, compare it with the requested one, ignore case
                if (i - start == wantLen && classAttr.regionMatches(true, start, className, 0, wantLen)) {
                    return true;
                }
                inClass = false;
            }
        } else {
            if (!inClass) {
                // we're in a class name : keep the start of the substring
                inClass = true;
                start = i;
            }
        }
    }
 
    // check the last entry
    if (inClass && len - start == wantLen) {
        return classAttr.regionMatches(true, start, className, 0, wantLen);
    }
 
    return false;
}

3. Имя тега в или нет?

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

01
02
03
04
05
06
07
08
09
10
11
// 1.7.2
} else if (StringUtil.in(name, "base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title")) {
    return tb.process(t, InHead);
}
 
// 1.7.3
static final String[] InBodyStartToHead = new String[]{"base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title"};
...
} else if (StringUtil.inSorted(name, Constants.InBodyStartToHead)) {
    return tb.process(t, InHead);
}

Согласно комментарию, написанному автором: «Здесь немного труднее читать, но он вызывает меньше GC, чем динамические переменные. Вносит около 10% разбора загрузки GC. Необходимо убедиться, что они отсортированы, как используется в findSorted ». Просто используя static final константный массив, также сделайте их сортированными так, чтобы двоичный поиск также улучшился с O (n) до O (log (n)), соотношение цены и производительности здесь довольно хорошее.

Однако «НЕОБХОДИМО обновить HtmlTreebuilderStateTest, если добавлено больше массивов», — это не хороший способ синхронизации IMHO, вместо того, чтобы копировать и вставлять, я бы использовал отражение для извлечения этих констант. Вы можете найти мое предложение в запросе на извлечение № 1157: «Упростите модульный тест состояния сортировки состояний — избегайте дублирования кода в HtmlTreeBuilderStateTest.java» .

4. Легкая модель

Вы знаете хитрость Integer.valueOf(i) ? Он поддерживает кэш IntegerCache от -128 до 127 или выше, если настроен ( java.lang.Integer.IntegerCache.high ), в результате == и equals результат будет другим, если значение находится в другом диапазоне (классический Вопрос интервью на Java?). Это пример Flyweight Pattern на самом деле. Что касается Jsoup, применение этого шаблона также сократит время создания объектов и увеличит производительность.

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
/**
 * Caches short strings, as a flywheel pattern, to reduce GC load. Just for this doc, to prevent leaks.
 * <p />
 * Simplistic, and on hash collisions just falls back to creating a new string, vs a full HashMap with Entry list.
 * That saves both having to create objects as hash keys, and running through the entry list, at the expense of
 * some more duplicates.
 */
private static String cacheString(final char[] charBuf, final String[] stringCache, final int start, final int count) {
    // limit (no cache):
    if (count > maxStringCacheLen)
        return new String(charBuf, start, count);
    if (count < 1)
        return "";
 
    // calculate hash:
    int hash = 0;
    int offset = start;
    for (int i = 0; i < count; i++) {
        hash = 31 * hash + charBuf[offset++];
    }
 
    // get from cache
    final int index = hash & stringCache.length - 1;
    String cached = stringCache[index];
 
    if (cached == null) { // miss, add
        cached = new String(charBuf, start, count);
        stringCache[index] = cached;
    } else { // hashcode hit, check equality
        if (rangeEquals(charBuf, start, count, cached)) { // hit
            return cached;
        } else { // hashcode conflict
            cached = new String(charBuf, start, count);
            stringCache[index] = cached; // update the cache, as recently used strings are more likely to show up again
        }
    }
    return cached;
}

Существует также другой сценарий минимизации новых GC StringBuilder, использующих ту же идею.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private static final Stack<StringBuilder> builders = new Stack<>();
 
/**
 * Maintains cached StringBuilders in a flyweight pattern, to minimize new StringBuilder GCs. The StringBuilder is
 * prevented from growing too large.
 * <p>
 * Care must be taken to release the builder once its work has been completed, with {@see #releaseBuilder}
*/
public static StringBuilder borrowBuilder() {
    synchronized (builders) {
        return builders.empty() ?
            new StringBuilder(MaxCachedBuilderSize) :
            builders.pop();
    }
}

На самом деле CharacterReader и StringUtil достойны того, чтобы переваривать их все больше и больше, поскольку есть много полезных советов и приемов, которые вас вдохновят.

5. Другие методы улучшения

  • Используйте RandomAccessFile для чтения файлов, которые улучшили время чтения файлов в 2 раза. Проверьте # 248 для более подробной информации
  • Рефакторинг иерархии узлов. Проверьте # 911 для более подробной информации
  • «Улучшения в значительной степени благодаря переупорядочению методов HtmlTreeBuilder на основе анализа различных веб-сайтов» — я перечисляю этот здесь, потому что это очень практично. Более глубокое понимание и наблюдение за тем, как будет выполняться код, также даст вам некоторое представление
  • Вызовите list.toArray(0) а не list.toArray(list.size()) — это использовалось в некоторых проектах с открытым исходным кодом, таких как h2database , поэтому я также предложил это в другом запросе Pull # 1158

6. Неизвестные

Оптимизация никогда не заканчивается. Есть еще много советов и приемов, которые я не обнаружил в это время. Я был бы признателен, если бы вы могли поделиться ими со мной, если вы найдете больше вдохновляющих идей в Jsoup. Вы можете найти мою контактную информацию на левой боковой панели этого веб-сайта или просто отправить электронное письмо по ny83427 at gmail.com .

-Продолжение следует-

Опубликовано на Java Code Geeks с разрешения Натанаэля Янга, партнера нашей программы JCG . См. Оригинальную статью здесь: « За кулисами» Секреты Jsoup V: Советы и хитрости по оптимизации

Мнения, высказанные участниками Java Code Geeks, являются их собственными.