15

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

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

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

我的问题:

  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 次运行的结果:

  • 没有变量,使用简短的打印语句
  • 没有变量,使用长打印语句(打印对象之一)
  • 顺序运行(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
4

4 回答 4

10

清除(错误)共享

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

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

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


该打印语句如何改变性能(特别是因为它出现在性能测量窗口之后)?

我猜还不够热,打印 GC 和编译日志,这样你就可以确定没有干扰并且代码已经被编译了。 java -server需要 10k 次迭代,最好不要全部在主循环中生成好的代码。

于 2012-08-07T15:20:33.623 回答
2

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

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

证明:注释掉 ArrayList 并放入:

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

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

于 2012-08-07T15:29:28.803 回答
1

这只是一个想法,我不知道如何验证它,但这可能与缓存有关。随着 ArrayList 的出现,您的对象变得更大,因此较少数量的对象适合某些给定的缓存内存区域,从而导致更多的缓存未命中。

你实际上可以尝试的是使用不同大小的 ArrayLists,从而改变你的类实例的内存占用,看看它是否对性能有影响。

于 2012-08-07T15:08:00.257 回答
1

相当有趣的旅程。这更像是“这是我的结果答案”。我怀疑/希望其他人会提出更好的回应。

您显然遇到了一些有趣的优化点。我怀疑objects.get(0).id在长println语句中添加了一些关于该id字段使用的优化。除了++没有其他用法,id因此优化器可能正在优化一些访问,volatile id从而提高速度。只需使用ida 访问该字段即可获得long x = objects.get(0).id;相同的性能提升。

这个List领域更有趣。如果添加了字段,则会发生相同的性能改进,private String foo = new String("weofjwe");但如果它没有private String foo = "weofjwe";创建对象,则不会,因为它"..."是在编译时完成的。我确信final是相关的,但似乎不是。我只能推测这与构造函数优化有关,并添加了new导致优化停止的原因,尽管我本来volatile会更有效地做到这一点。

于 2012-08-07T15:13:14.573 回答