1 янв. 2011 г.

Lock Coarsening, Biased Locking, Escape Analysis and others

Оптимизационные трюки, используемые при динамической компиляции jit'ом в java 6. В java 6 была существенно изменена (по сравнению с java 5) работа блокировок, существенно облегчив их, в java6u14 появился escape-анализ, который работает по-умолчанию, а так же много других интересных подходов.
  • Lock coarsening, Eliminate locks - укрупнение блокировок

    -XX:+EliminateLocks

    Данная опция, относящаяся к оптимизации многопоточных приложений, не удаляет блокировки в целом - имеет место устранение излишних блокировок в результате их объединения, и в некотором смысле их загрубление.
    Несколько смежных блоков, которые выполняются под одним и тем же монитором могут быть объединены в блок с одним захватом монитора.
    Тем самым происходит устранение излишних разблокирований и повторных захватов мониторов.
    Если между двумя захватами одного и того же монитора находится блок не под монитором, дабы уменьшить число захватов он тоже окажется под монитором - это и есть загрубление блокировок (lock coarsening).

    -XX:+EliminateLocks
    Устранение лишних блокировок - Lock coarsening

    На примере показано как устраняется overhead на захват монитора.
  • Biased Locking - Смещение блокировок

    -XX:+UseBiasedLocking

    Обновлено.. Краткий пересказ Biased locking в рабочих заметках

    Механизм biased locking основывается на свойстве, что большинство объектов захватывают монитор на очень малое время, так, что даже при большом количестве потоков contention оказывается весьма мал.



    -XX:-UseBiasedLocking
    Иллюстрация работы с отключённым biased locking.

    Для этого в JVM были реализованы spin-locks (thin locks), используется ownerThreadId, и, пытаясь захватить монитор, CAS(-1, threadId). Если CAS удался — захватили монитор, иначе делаеться еще несколько попыток в цикле (вдруг нынешний владелец отпустит монитор буквально через пару тактов). Количество итераций это предмет тонкого тюнинга, возможно даже адаптивного, на базе профиля предыдущих попыток. Если даже со спиннингом мы не захватили монитор — откатываемся к OS-specific locks (fat-locks).


    -XX:+UseBiasedLocking

  • Escape analysis - Анализ локальности

    -XX:+DoEscapeAnalysis

    Цель escape-анализа - определить локализован ли указатель создаваемого объекта внутри блока объекта или он выходит (escape) за рамки данного блока (в другую нить (thread), может быть возвращен из блока, а также, когда указатель помещён в глобальную переменную).

    Исследование данной проблемы длится не один год, одна из подробных статей Escape analysis for Java была представлена в 1999 году.

    В java каждая нить (thread) имеет свой собственный кусок памяти - стек (stack), который хранит фреймы (историю вызовов методов), частичные результаты операций, локальные переменные (примитивы, в т.ч. и указатели/ссылки на объекты). В этом смысле java стек очень похож на стек C++, за исключением того, что в java стеком нельзя напрямую оперировать и создавать на нём объекты, как в C++.

    Куча (heap), общая для всех нитей область памяти, используется для хранения всех создаваемых объектов.

    -XX:-DoEscapeAnalysis
    Объекты создаются на общей куче

    Благодаря escape-анализу объекты могут быть созданы прямо на стеке или размещены в регистрах (используя register allocator). Не смотря на то , что создание объекта на кучи всё достаточно дешёвая операция - есть обратная сторона - Garbage Collector. Для высоконагруженных приложений даже вызов minor gc это существенные затраты. Объект, созданный на стеке, будет уничтожен, когда он покинет stack frame, в котором он был создан, как следствие - отсутствие нагрузки на (minor) GC.

    -XX:+DoEscapeAnalysis

    Более того - нет необходимости использовать блокировку для локализованных объектов - т.к. жизненный цикл объекта ограничен одним блоком и никакая другая нить не может его «увидеть» или использовать (прямое следствие из определения pointer escape) .

    В качестве примера используем следующий microbenchmark:
    import java.util.*;
    
    public class Foo {
    public static void main(String[] args){
    final int warmup = 100 * 1000; 
    final int run = 1000 * 1000; 
    foo(warmup);
    final long start = System.nanoTime();
    foo(run);
    final long end = System.nanoTime();
    System.out.println((end - start) / 1000 / 1e3);
    }
    
    private static int foo(int count){
    int s = 0; 
    for(int i = 0; i < count; i++){
    Collection c = new ArrayList();
    s += c.size();
    }
    return s;
    }
    }
    $ java -XX:-DoEscapeAnalysis -verbose:gc -XX:+PrintGCDetails Foo
    [GC [PSYoungGen: 12160K->152K(14144K)] 12160K->152K(46528K), 0.0019620 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
    [GC [PSYoungGen: 12312K->136K(26304K)] 12312K->136K(58688K), 0.0008050 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [GC [PSYoungGen: 24456K->136K(26304K)] 24456K->136K(58688K), 0.0006690 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [GC [PSYoungGen: 24456K->136K(50624K)] 24456K->136K(83008K), 0.0005730 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    65.78
    Heap
    PSYoungGen      total 50624K, used 14728K [0xa3e20000, 0xa7180000, 0xb3ac0000)
    eden space 48640K, 30% used [0xa3e20000,0xa4c602a8,0xa6da0000)
    from space 1984K, 6% used [0xa6f90000,0xa6fb2050,0xa7180000)
    to   space 1984K, 0% used [0xa6da0000,0xa6da0000,0xa6f90000)
    PSOldGen        total 32384K, used 0K [0x844c0000, 0x86460000, 0xa3e20000)
    object space 32384K, 0% used [0x844c0000,0x844c0000,0x86460000)
    PSPermGen       total 16384K, used 1900K [0x804c0000, 0x814c0000, 0x844c0000)
    object space 16384K, 11% used [0x804c0000,0x8069b048,0x814c0000)
    $ java -XX:+DoEscapeAnalysis -verbose:gc -XX:+PrintGCDetails Foo
    0.747
    Heap
    PSYoungGen      total 14144K, used 4135K [0xa3ed0000, 0xa4e90000, 0xb3b70000)
    eden space 12160K, 34% used [0xa3ed0000,0xa42d9d90,0xa4ab0000)
    from space 1984K, 0% used [0xa4ca0000,0xa4ca0000,0xa4e90000)
    to   space 1984K, 0% used [0xa4ab0000,0xa4ab0000,0xa4ca0000)
    PSOldGen        total 32384K, used 0K [0x84570000, 0x86510000, 0xa3ed0000)
    object space 32384K, 0% used [0x84570000,0x84570000,0x86510000)
    PSPermGen       total 16384K, used 1893K [0x80570000, 0x81570000, 0x84570000)
    object space 16384K, 11% used [0x80570000,0x807496f0,0x81570000)

    и такой же тест, для java.util.Vector

    ArrayListVector
    -EA+EA-EA+EA
    время выполнения, ms:65.780.747115.8050.83
    срабатываний GC:4050
    время работы GC, ms:4-4.5-


    Как и ожидалось время выполнения значительно меньше, меньше вызовов GC, который удаляет объекты «как только так сразу» из young geneneration. Так же заметно ускорение работы thread-safe «версии» ArrayList'а - java.util.Vector.

    Однако, не стоит надеяться, что благодаря escape-анализу производительность увеличится столь же стремительно как в данном синтетическом тесте. Определённо можно сказать, что нагрузка на GC будет значительно меньше для большинства типов приложений.


    график нагрузки gc реального приложения
    до использования java6u14 и после,
    по вертикали - суммарное время проведённое в gc





  • Оптимизация конкатенации строк

    -XX:+OptimizeStringConcat (java 6u20+).

    Несколько типичных шаблонов по конкатенации можно оптимизировать, например, String использует тот же объект char[] (т.е та же ссылка/указатель) который хранит StringBuffer или StringBuilder:
    The end result is the saving of an unnecessary char[] allocation and a subsequent call to arraycopy.

    Рассмотрим в деталях, что происходит, при формировании, например, сообщения:
    Более детально стоит обратить, что происходит в методе java.lang.StringBuilder.toString():
        public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
    }
    и далее в конструкторе java.lang.String:
        public String(char value[], int offset, int count) {
    if (offset < 0) {
    throw new StringIndexOutOfBoundsException(offset);
    }
    if (count < 0) {
    throw new StringIndexOutOfBoundsException(count);
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
    throw new StringIndexOutOfBoundsException(offset + count);
    }
    
    this.offset = 0;
    this.count = count;
    this.value = v;
    }





  • Использование byte[] вместо char[] для строки содержащей исключительно ASCII символы.

    -XX:+UseCompressedStrings (java6u21)

    java.lang.String по своей сути является обёрткой вокруг массива char[], делая его защищённым и не изменяемым. char занимает 2 байта. Если же строки используют исключительно ASCII символы, т.е ограничены диапазоном одного байта - используя данную оптимизацию немного сэкономить память.

    Однако, не стоит ожидать, что объём используемой памяти сильно сократится.





Ссылки:

Комментариев нет: