如果执行顺序(几乎)不受影响,如何在严重的性能下降中分配变量结果?

ssc*_*ieb 19 java performance multithreading jvm volatile

在玩多线程时,我可以观察到与AtomicLong(以及使用它的类,例如java.util.Random)相关的一些意外但严重的性能问题,我目前没有解释.但是,我创建了一个简约示例,它基本上由两个类组成:一个类"Container",它保存对volatile变量的引用;一个类"DemoThread",它在线程执行期间对"Container"实例进行操作.请注意,对"Container"和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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在我的测试中,我重复创建了4个DemoThreads,然后启动并加入.每个循环的唯一区别是"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);
    }
Run Code Online (Sandbox Code Playgroud)

由于某种原因,如果在启动线程之前立即执行prepare(),则需要两倍的时间才能完成,即使没有"volatile"关键字,性能差异也很大,至少在两台机器和操作系统上我测试了代码.这是一个简短的总结:


Mac OS摘要:

Java版本:1.6.0_24
Java类版本:50.0
VM供应商:Sun Microsystems Inc.
VM版本:19.1-b02-334
VM名称:Java HotSpot(TM)64位服务器VM
OS名称:Mac OS X
OS Arch:x86_64
OS版本:10.6.5
处理器/内核:8

使用volatile关键字:
最终结果:
31979 ms.当实例化后调用prepare()时.
96482毫秒 在执行之前调用prepare()时.

没有volatile关键字:
最终结果:
26009毫秒.当实例化后调用prepare()时.
35196毫秒 在执行之前调用prepare()时.


Windows摘要:

Java版本:1.6.0_24
Java类版本:50.0
VM供应商:Sun Microsystems Inc.
VM版本:19.1-b02
VM名称:Java HotSpot(TM)64位服务器VM
OS名称:Windows 7
OS Arch:amd64
OS版本:6.1
处理器/核心:4

使用volatile关键字:
最终结果:
18120 ms.当实例化后调用prepare()时.
36089毫秒 在执行之前调用prepare()时.

没有volatile关键字:
最终结果:
10115毫秒.当实例化后调用prepare()时.
10039毫秒.在执行之前调用prepare()时.


Linux摘要:

Java版本:1.6.0_20
Java类版本:50.0
VM供应商:Sun Microsystems Inc.
VM版本:19.0-b09
VM名称:OpenJDK 64位服务器VM
OS名称:Linux
OS Arch:amd64
OS版本:2.6.32-28-通用
处理器/核心:4

使用volatile关键字:
最终结果:
45848 ms.当实例化后调用prepare()时.
110754毫秒 在执行之前调用prepare()时.

没有volatile关键字:
最终结果:
37862毫秒.当实例化后调用prepare()时.
39357毫秒 在执行之前调用prepare()时.


Mac OS详细信息(易失性):

测试1,4线程,在创建循环中设置变量
Thread-2在653 ms后完成.
线程-3在653毫秒后完成.
线程-4在653毫秒后完成.
线程-5在653毫秒后完成.
总时间:654毫秒.

测试2,4线程,在启动循环中设置变量
Thread-7在1588 ms后完成.
线程-6在1589 ms后完成.
线程-8在1593 ms后完成.
线程9在1593 ms后完成.
总时间:1594毫秒.

测试3,4个线程,在创建循环中设置变量
Thread-10在648 ms后完成.
线程12在648 ms后完成.
线程-13在648 ms后完成.
线程-11在648 ms后完成.
总时间:648毫秒.

测试4个,4个线程,在启动循环中设置变量
Thread-17在1353 ms后完成.
Thread-16在1957 ms之后完成.
线程14在2170毫秒后完成.
线程-15在2169 ms后完成.
总时间:2172毫秒.

(等等,有时'慢'循环中的一个或两个线程按预期完成,但大多数情况下它们没有完成).

给定的例子在理论上看起来没用,而且这里不需要'volatile' - 但是,如果你使用'java.util.Random'-Instance而不是'Container'-Class并调用,例如,nextInt()多次,会发生相同的效果:如果在Thread的构造函数中创建对象,线程将快速执行,但如果在run() - 方法中创建它,则会很慢.我相信一年多前在Mac OS上Java随机减速中描述的性能问题与这种效果有关,但我不知道它为什么会这样 - 除此之外我确信它不应该像因为这意味着在线程的run-method中创建一个新对象总是很危险的,除非你知道在对象图中不会涉及任何volatile变量.分析没有帮助,因为在这种情况下问题消失了(与Mac OS续约中的Java随机减速一样),并且它也不会发生在单核PC上 - 所以我猜它是一种线程同步问题...然而,奇怪的是,实际上没有什么可以同步,因为所有变量都是线程本地的.

真的期待任何提示 - 如果您想确认或伪造问题,请参阅下面的测试用例.

谢谢,

斯蒂芬

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

}

irr*_*ble 16

它可能是两个易变的变量a并且b彼此太靠近,它们属于同一个缓存行; 虽然CPU A只读/写变量a,而CPU B只读/写变量b,但它们仍然通过同一缓存线相互耦合.这些问题被称为虚假共享.

在您的示例中,我们有两个分配方案:

new Thread                               new Thread
new Container               vs           new Thread
new Thread                               ....
new Container                            new Container
....                                     new Container
Run Code Online (Sandbox Code Playgroud)

在第一种方案中,两个易变量变量彼此接近的可能性很小.在第二个方案中,几乎可以肯定.

CPU缓存不适用于单个单词; 相反,他们处理缓存行.高速缓存行是连续的内存块,比如64个相邻字节.通常这很好 - 如果CPU访问一个单元,它很可能也会访问相邻的单元.除了在您的示例中,该假设不仅无效,而且有害.

假设ab落入相同的缓存行L.当CPU A更新时a,它会通知其他L脏的CPU .既然B缓存L,因为它正在工作b,B必须删除它的缓存L.所以下次B需要阅读b,它必须重新加载L,这是昂贵的.

如果B必须访问主内存重新加载,这是非常昂贵的,它通常慢100倍.

幸运的是,AB可以直接对新值,而无需通过主内存去沟通.然而,这需要额外的时间.

为了验证这个理论,你可以填充额外的128个字节Container,这样两个两个易失性变量Container就不会落入同一个缓存行; 那么你应该注意到这两个方案需要大约相同的时间才能执行.

学到的经验:通常CPU假设adjecent变量是相关的.如果我们想要自变量,我们最好将它们彼此远离.


Jon*_*eet 7

好吧,你正在写一个易变的变量,所以我怀疑这会强制内存障碍 - 撤消一些可以实现的优化.JVM不知道在另一个线程上不会观察到该特定字段.

编辑:如上所述,基准测试本身存在问题,例如计时器运行时的打印.此外,在开始计时之前"预热"JIT通常是一个好主意 - 否则你要测量的时间在正常的长时间运行过程中并不重要.