性能说明:使用未使用的变量时,代码运行速度更快

2022-09-03 15:00:54

我之前做了一些性能测试,无法解释我得到的结果。

运行下面的测试时,如果我取消注释,性能会显著提高。在我的计算机上,当该字段存在时,测试在 70-90 毫秒内运行,而当它被注释掉时,测试的运行时间为 650 毫秒。private final List<String> list = new ArrayList<String>();

我还注意到,如果我将 print 语句更改为 ,则不带变量的测试将在 450-500 ms 而不是 650 ms 内运行。当变量存在时,它不起作用。System.out.println((end - start) / 1000000);

我的问题:

  1. 有或没有变量,任何人都可以解释近10的因子,考虑到我甚至不使用该变量?
  2. 该打印语句如何更改性能(特别是因为它位于性能度量窗口之后)?

ps:按顺序运行时,3个场景(带变量,无变量,使用不同的打印语句)都需要大约260ms。

public class SOTest {

    private static final int ITERATIONS = 10000000;
    private static final int THREADS = 4;

    private volatile long id = 0L;
    //private final List<String> list = new ArrayList<String>();

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(THREADS);
        final List<SOTest> objects = new ArrayList<SOTest>();
        for (int i = 0; i < THREADS; i++) {
            objects.add(new SOTest());
        }

        //warm up
        for (SOTest t : objects) {
            getRunnable(t).run();
        }

        long start = System.nanoTime();

        for (SOTest t : objects) {
            executor.submit(getRunnable(t));
        }
        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);

        long end = System.nanoTime();
        System.out.println(objects.get(0).id + " " + (end - start) / 1000000);
    }

    public static Runnable getRunnable(final SOTest object) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < ITERATIONS; i++) {
                    object.id++;
                }
            }
        };
        return r;
    }
}

编辑

请参阅下面 3 个方案的 10 次运行的结果:

  • 不带变量,使用短 print 语句
  • 不带变量,使用长 print 语句(打印其中一个对象)
  • 顺序运行(1 个线程)
  • 与变量
1   657 473 261 74
2   641 501 261 78
3   651 465 259 86
4   585 462 259 78
5   639 506 259 68
6   659 477 258 72
7   653 479 259 82
8   645 486 259 72
9   650 457 259 78
10  639 487 272 79

答案 1

清除(错误)共享

由于内存中的布局,对象共享缓存行...它已经解释了很多次(甚至在这个网站上):这是一个很好的来源,可以进一步阅读。这个问题同样适用于 C#(或 C/C++)

当您通过添加注释掉的行来填充对象时,共享会减少,并且您会看到性能的提升。

编辑:我错过了第二个问题:


该打印语句如何更改性能(特别是因为它位于性能度量窗口之后)?

我想没有足够的预热,打印GC和编译日志,这样你就可以确保没有干扰,代码实际上是编译的。 需要10k次迭代,最好不要全部在主循环中生成良好的代码。java -server


答案 2

您正在达到执行硬件的微妙效果。SOTest 对象的内存非常小,因此所有 4 个实例都可能适合内存中的同一缓存行。由于您使用的是易失性,这将导致不同内核之间的缓存垃圾(只有一个内核可能具有脏缓存行)。

当您在 ArrayList 中进行注释时,内存布局会发生变化(ArrayList 是在 SOTest 实例之间创建的),并且易失性字段现在进入不同的缓存行。CPU的问题消失了,因此性能飙升。

证明:注释掉 ArrayList 并改为放入:

long waste1, waste2, waste3, waste4, waste5, waste6, waste7, waste8;

这会将 SOTest 对象放大 64 个字节(奔腾处理器上一个高速缓存行的大小)。性能现在与 ArrayList 中的性能相同。


推荐