Как присвоение переменной может привести к серьезному падению производительности, в то время как порядок выполнения (почти) не изменился?

Играя с многопоточностью, я мог наблюдать некоторые неожиданные, но серьезные проблемы с производительностью, связанные с AtomicLong ( и использующие его классы, такие как java.util.Random), для которых у меня в настоящее время нет объяснений. Однако я создал минималистичный пример, который в основном состоит из двух классов: класса «Контейнер», который хранит ссылку на изменчивую переменную, и класса «DemoThread», который работает с экземпляром «Контейнер». во время выполнения потока. Обратите внимание, что ссылки на «Контейнер» и volatile long являются частными и никогда не используются совместно между потоками (я знаю, что здесь нет необходимости использовать volatile, это просто для демонстрационных целей) - таким образом, несколько экземпляров «DemoThread» должны работать идеально. параллельно на многопроцессорной машине, но по какой-то причине они этого не делают (полный пример находится внизу этого поста).

private static class Container  {

    private volatile long value;

    public long getValue() {
        return value;
    }

    public final void set(long newValue) {
        value = newValue;
    }
}

private static class DemoThread extends Thread {

    private Container variable;

    public void prepare() {
        this.variable = new Container();
    }

    public void run() {
        for(int j = 0; j < 10000000; j++) {
            variable.set(variable.getValue() + System.nanoTime());
        }
    }
}

Во время моего теста я неоднократно создавал 4 DemoThread, которые затем запускались и объединялись. Единственная разница в каждом цикле - это время, когда вызывается "prepare ()" (который, очевидно, требуется для запуска потока, поскольку в противном случае это привело бы к исключению NullPointerException):

DemoThread[] threads = new DemoThread[numberOfThreads];
    for(int j = 0; j < 100; j++) {
        boolean prepareAfterConstructor = j % 2 == 0;
        for(int i = 0; i < threads.length; i++) {
            threads[i] = new DemoThread();
            if(prepareAfterConstructor) threads[i].prepare();
        }

        for(int i = 0; i < threads.length; i++) {
            if(!prepareAfterConstructor) threads[i].prepare();
            threads[i].start();
        }
        joinThreads(threads);
    }

По какой-то причине, если prepare () является выполняется непосредственно перед запуском потока, для завершения потребуется в два раза больше времени, и даже без ключевого слова volatile разница в производительности была значительной, по крайней мере, на двух машинах и ОС, код которых я тестировал. Вот краткое описание:


Обзор Mac OS:

Версия Java: 1.6.0_24
Версия класса Java: 50.0
Производитель ВМ: Sun Microsystems Inc.
Версия VM: 19.1-b02-334
Имя виртуальной машины: 64-разрядная серверная виртуальная машина Java HotSpot (TM)
Название ОС: Mac OS X
Архитектура ОС: x86_64
Версия ОС: 10.6.5
Процессоров / ядер: 8

С ключевым словом volatile:
Окончательные результаты:
31979 мс. когда метод prepare () был вызван после создания экземпляра.
96482 мс. когда метод prepare () был вызван перед выполнением.

Без изменчивого ключевого слова:
Окончательные результаты:
26009 мс. когда метод prepare () был вызван после создания экземпляра.
35196 мс. когда метод prepare () был вызван перед выполнением.


Обзор Windows:

Версия Java: 1.6.0_24
Версия класса Java: 50.0
Производитель ВМ: Sun Microsystems Inc.
Версия VM: 19.1-b02
Имя виртуальной машины: 64-разрядная серверная виртуальная машина Java HotSpot (TM)
Имя ОС: Windows 7
Архитектура ОС: amd64
Версия ОС: 6.1
Процессоров / ядер: 4

с ключевым словом volatile:
Окончательные результаты:
18120 мс. когда метод prepare () был вызван после создания экземпляра.
36089 мс. когда метод prepare () был вызван перед выполнением.

Без изменчивого ключевого слова:
Окончательные результаты:
10115 мс. когда метод prepare () был вызван после создания экземпляра.
10039 мс. когда метод prepare () был вызван перед выполнением.


Обзор Linux:

Версия Java: 1.6.0_20
Версия класса Java: 50.0
Производитель ВМ: Sun Microsystems Inc.
Версия VM: 19.0-b09
Имя виртуальной машины: 64-разрядная серверная виртуальная машина OpenJDK
Имя ОС: Linux
Архитектура ОС: amd64
Версия ОС: 2.6.32-28-generic
Процессоров / ядер: 4

С ключевым словом volatile:
Окончательные результаты:
45848 мс. когда метод prepare () был вызван после создания экземпляра.
110754 мс. когда метод prepare () был вызван перед выполнением.

Без изменчивого ключевого слова:
Окончательные результаты:
37862 мс. когда метод prepare () был вызван после создания экземпляра.
39357 мс. когда метод prepare () был вызван перед выполнением.


Сведения о Mac OS (изменчиво):

Тест 1, 4 потока, установка переменной в цикле создания
Поток-2 завершен через 653 мс.
Поток 3 завершен через 653 мс.
Поток-4 завершен через 653 мс.
Поток 5 завершен через 653 мс.
Общее время: 654 мс.

Тест 2, 4 потока, установка переменной в цикле запуска
Поток 7 завершен через 1588 мс.
Поток-6 завершен через 1589 мс.
Поток 8 завершен через 1593 мс.
Поток-9 завершен через 1593 мс.
Общее время: 1594 мс.

Тест 3, 4 потока, установка переменной в цикле создания
Поток-10 завершен через 648 мс.
Поток-12 завершен через 648 мс.
Поток 13 завершен через 648 мс.
Поток-11 завершен через 648 мс.
Общее время: 648 мс.

Тест 4, 4 потока, установка переменной в цикле запуска
Поток-17 завершен через 1353 мс.
Поток-16 завершен после 1957 мс.
Поток-14 завершен через 2170 мс.
Поток-15 завершен через 2169 мс.
Общее время: 2172 мс.

(и так далее, иногда один или два потока в «медленном» цикле завершаются, как ожидалось, но в большинстве случаев это не так).

Данный пример выглядит теоретически, так как бесполезен, и 'volatile' здесь не требуется - однако, если вы используете 'java.util.Random'-Instance вместо 'Container'-Class и вызовете, например, nextInt () несколько раз, те же эффекты будут происходят: поток будет выполняться быстро, если вы создаете объект в конструкторе потока, но медленно, если вы создаете его в методе run (). Я считаю, что проблемы с производительностью, описанные в Java Random Slowdown on Mac OS более года назад, связаны с этим эффектом, но я понятия не имею, почему это так, - кроме того, я уверен, что так не должно быть, поскольку это означало бы, что всегда опасно создавать новый объект в методе выполнения потока, если вы не знаете, что никакие изменчивые переменные не будут задействованы в графе объекта. Профилирование не помогает, так как в этом случае проблема исчезает (то же наблюдение, что и в Случайное замедление Java в Mac OS, продолжение ), и этого также не происходит на одноядерном ПК - поэтому Я предполагаю, что это своего рода проблема синхронизации потоков ... однако странно то, что на самом деле синхронизировать нечего, поскольку все переменные являются локальными для потока.

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

Спасибо,

Стефан

public class UnexpectedPerformanceIssue {

private static class Container  {

    // Remove the volatile keyword, and the problem disappears (on windows)
    // or gets smaller (on mac os)
    private volatile long value;

    public long getValue() {
        return value;
    }

    public final void set(long newValue) {
        value = newValue;
    }
}

private static class DemoThread extends Thread {

    private Container variable;

    public void prepare() {
        this.variable = new Container();
    }

    @Override
    public void run() {
        long start = System.nanoTime();
        for(int j = 0; j < 10000000; j++) {
            variable.set(variable.getValue() + System.nanoTime());
        }
        long end = System.nanoTime();
        System.out.println(this.getName() + " completed after "
                +  ((end - start)/1000000) + " ms.");
    }
}

public static void main(String[] args) {
    System.out.println("Java Version: " + System.getProperty("java.version"));
    System.out.println("Java Class Version: " + System.getProperty("java.class.version"));

    System.out.println("VM Vendor: " + System.getProperty("java.vm.specification.vendor"));
    System.out.println("VM Version: " + System.getProperty("java.vm.version"));
    System.out.println("VM Name: " + System.getProperty("java.vm.name"));

    System.out.println("OS Name: " + System.getProperty("os.name"));
    System.out.println("OS Arch: " + System.getProperty("os.arch"));
    System.out.println("OS Version: " + System.getProperty("os.version"));
    System.out.println("Processors/Cores: " + Runtime.getRuntime().availableProcessors());

    System.out.println();
    int numberOfThreads = 4;

    System.out.println("\nReference Test (single thread):");
    DemoThread t = new DemoThread();
    t.prepare();
    t.run();

    DemoThread[] threads = new DemoThread[numberOfThreads];
    long createTime = 0, startTime = 0;
    for(int j = 0; j < 100; j++) {
        boolean prepareAfterConstructor = j % 2 == 0;
        long overallStart = System.nanoTime();
        if(prepareAfterConstructor) {
            System.out.println("\nTest " + (j+1) + ", " + numberOfThreads + " threads, setting variable in creation loop");             
        } else {
            System.out.println("\nTest " + (j+1) + ", " + numberOfThreads + " threads, setting variable in start loop");
        }

        for(int i = 0; i < threads.length; i++) {
            threads[i] = new DemoThread();
            // Either call DemoThread.prepare() here (in odd loops)...
            if(prepareAfterConstructor) threads[i].prepare();
        }

        for(int i = 0; i < threads.length; i++) {
            // or here (in even loops). Should make no difference, but does!
            if(!prepareAfterConstructor) threads[i].prepare();
            threads[i].start();
        }
        joinThreads(threads);
        long overallEnd = System.nanoTime();
        long overallTime = (overallEnd - overallStart);
        if(prepareAfterConstructor) {
            createTime += overallTime;
        } else {
            startTime += overallTime;
        }
        System.out.println("Overall time: " + (overallTime)/1000000 + " ms.");
    }
    System.out.println("Final results:");
    System.out.println(createTime/1000000 + " ms. when prepare() was called after instantiation.");
    System.out.println(startTime/1000000 + " ms. when prepare() was called before execution.");
}

private static void joinThreads(Thread[] threads) {
    for(int i = 0; i < threads.length; i++) {
        try {
            threads[i].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

}

19
задан Community 23 May 2017 в 12:07
поделиться