Статьи

Предвзятая блокировка, OSR и бенчмаркинг

После моего последнего поста о реализации блокировок Java я получил много хороших отзывов о моих результатах и ​​подходе к дизайну микро-тестов. В результате теперь я понимаю, что прогрев JVM, замена на стеке (OSR) и смещенная блокировка несколько лучше, чем раньше. Особая благодарность Dave Dice из Oracle и Cliff Click & Gil Tene из Azul за их очень полезную обратную связь.

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

В этом посте я перезапущу эксперимент с учетом обратной связи и представлю некоторые новые результаты. Я также подробно остановлюсь на изменениях, внесенных в тест, и на том, почему важно учитывать поведение разогрева JVM при написании микро-тестов или даже очень скудных приложений Java с быстрым временем запуска.

При замене стека (OSR)

виртуальные машины Java будут компилировать код для достижения большей производительности на основе профилирования во время выполнения. Некоторые виртуальные машины запускают интерпретатор для большей части кода и заменяют горячие области скомпилированным кодом, следуя правилу 80/20. Другие виртуальные машины сначала компилируют весь код, а затем заменяют простой код более оптимизированным кодом на основе профилирования. Oracle Hotspot и Azul являются примерами первого типа, а Oracle JRockit — примером второго.

Oracle Hotspot будет подсчитывать вызовы метода return плюс ответвления для циклов в этом методе, и если он превышает 10K в режиме сервера, метод будет скомпилирован. Скомпилированный код в обычном JIT’инге может быть использован при следующем вызове метода. Однако, если цикл все еще повторяется, может иметь смысл заменить метод до его завершения, особенно если у него много итераций. OSR — это средство, с помощью которого метод заменяется скомпилированной версией на протяжении всего цикла итерации.

У меня сложилось впечатление, что нормальный JIT’ing и OSR приведут к схожему коду. Клифф Клик отметил, что во время выполнения гораздо труднее оптимизировать цикл на полпути, и особенно сложно, если он вложен. Например, проверка границ внутри цикла может оказаться невозможной для устранения. Клифф будетблог более подробно об этом в ближайшее время.

Это означает, что вы, вероятно, получите более оптимизированный код, выполнив небольшое количество более коротких прогонов, чем один большой. В приведенном ниже коде вы можете видеть, как я делаю 10 коротких циклов в цикле перед основным большим циклом по сравнению с предыдущей статьей, где я выполнял один большой прогрев.

Смещенная блокировка

Дейв Дайс отметил, что Hotspot не позволяет объектам смещенной блокировки в первые несколько секунд (в настоящее время 4 с) запуска JVM. Это связано с тем, что в некоторых тестах и ​​в NetBeans при запуске запускается много потоков, а стоимость отзыва значительна. Другие виртуальные машины, такие как Azul, используют с самого начала смещенную блокировку, что не является проблемой, поскольку их отзыв дешевый.

Все объекты по умолчанию создаются с включенной смещенной блокировкой в ​​Oracle Hotspot после первых нескольких секунд задержки запуска и могут быть настроены с -XX: BiasedLockingStartupDelay = 0 .

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

Код

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CyclicBarrier;

import static java.lang.System.out;

public final class TestLocks implements Runnable
{
    public enum LockType { JVM, JUC }
    public static LockType lockType;

    public static final long WARMUP_ITERATIONS = 100L * 1000L;
    public static final long ITERATIONS = 500L * 1000L * 1000L;
    public static long counter = 0L;

    public static final Object jvmLock = new Object();
    public static final Lock jucLock = new ReentrantLock();
    private static int numThreads;

    private final long iterationLimit;
    private final CyclicBarrier barrier;

    public TestLocks(final CyclicBarrier barrier, final long iterationLimit)
    {
        this.barrier = barrier;
        this.iterationLimit = iterationLimit;
    }

    public static void main(final String[] args) throws Exception
    {
        lockType = LockType.valueOf(args[0]);
        numThreads = Integer.parseInt(args[1]);

        for (int i = 0; i < 10; i++)
        {
            runTest(numThreads, WARMUP_ITERATIONS);
            counter = 0L;
        }

        final long start = System.nanoTime();
        runTest(numThreads, ITERATIONS);
        final long duration = System.nanoTime() - start;

        out.printf("%d threads, duration %,d (ns)\n", numThreads, duration);
        out.printf("%,d ns/op\n", duration / ITERATIONS);
        out.printf("%,d ops/s\n", (ITERATIONS * 1000000000L) / duration);
        out.println("counter = " + counter);
    }

    private static void runTest(final int numThreads, final long iterationLimit)
        throws Exception
    {
        CyclicBarrier barrier = new CyclicBarrier(numThreads);
        Thread[] threads = new Thread[numThreads];

        for (int i = 0; i < threads.length; i++)
        {
            threads[i] = new Thread(new TestLocks(barrier, iterationLimit));
        }

        for (Thread t : threads)
        {
            t.start();
        }

        for (Thread t : threads)
        {
            t.join();
        }
    }

    public void run()
    {
        try
        {
            barrier.await();
        }
        catch (Exception e)
        {
            // don't care
        }

        switch (lockType)
        {
            case JVM: jvmLockInc(); break;
            case JUC: jucLockInc(); break;
        }
    }

    private void jvmLockInc()
    {
        long count = iterationLimit / numThreads;
        while (0 != count--)
        {
            synchronized (jvmLock)
            {
                ++counter;
            }
        }
    }

    private void jucLockInc()
    {
        long count = iterationLimit / numThreads;
        while (0 != count--)
        {
            jucLock.lock();
            try
            {
                ++counter;
            }
            finally
            {
                jucLock.unlock();
            }
        }
    }
}

Скрипт для запуска тестов:

set -x
for i in {1..8}
do 
    java -server -XX:-UseBiasedLocking TestLocks JVM $i
done

for i in {1..8}
do 
    java -server -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 TestLocks JVM $i
done

for i in {1..8}
do 
    java -server TestLocks JUC $i
done 

Результаты

Тесты проводятся с 64-битным Linux (Fedora Core 15) и Oracle JDK 1.6.0_29.

Nehalem 2,8 ГГц — операций / сек
Потоки -UseBiasedLocking + UseBiasedLocking ReentrantLock
1 53283461
450950969
62876566
2 18519295
18108615
10217186
3 13349605
13416198
14108622
4 8120172
8040773
14207310
5 4725114
4551766
14302683
6 5133706
5246548
14676616
7 5473652
5585666
18145525
8 5514056
5414171
19010725
Sandybridge 2.0 ГГц — число операций в секунду
Потоки -UseBiasedLocking + UseBiasedLocking ReentrantLock
1
34500407
396511324
43148808
2
20899076
19742639
6038923
3
9288039
11957032
24147807
4
5618862
5589289
9082961
5
5609932
5592574
9389243
6
5742907
5760558
12518728
7
6699201
6641886
13684475
8
6957824
6925410
14819005

наблюдения

  1. Смещенная блокировка имеет огромное преимущество в неконтролируемом однопоточном корпусе.
  2. Смещенная блокировка, когда она не оспаривается и не отменяется, добавляет только 4-5 циклов затрат. Это стоимость при попадании в кэш для структур блокировки поверх кода, защищенного в критическом разделе.
  3. -XX: BiasedLockingStartupDelay = 0 необходимо установить для экономичных приложений и микропроцессоров.
  4. Отказ от ЛАРН не имеет существенного значения для этого набора результатов испытаний. Вероятно, это связано с тем, что цикл настолько прост или доминируют другие затраты.
  5. Для текущих реализаций ReentrantLocks масштабируется лучше, чем синхронизированные блокировки в условиях конкуренции, за исключением случая с двумя конкурирующими потоками.

Заключение

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

 

От http://mechanical-sympathy.blogspot.com/2011/11/biased-locking-osr-and-benchmarking-fun.html