【问题标题】:Performance explanation: code runs faster with unused variable性能说明:未使用变量的代码运行速度更快
【发布时间】:2012-08-04 14:34:25
【问题描述】:

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

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

我还注意到,如果我将打印语句更改为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;
    }
}

编辑

请看下面 10 次运行 3 种场景的结果:

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

【问题讨论】:

  • 您多久重复一次测试?这些时间是平均值吗?
  • @Baz 是的,这些是几次运行的平均值(比如 10-15 次运行),从一次运行到另一次运行结果相当稳定。
  • statistically prove确实不一样吗?可能只是“随机性”产生的噪音。如果你不这样做,我会首先尝试重复实验并收集足够大的样本来对数据进行统计声明。这并不费力,并且可以防止很多头痛(在其他情况下,如果不在此处)。
  • 您是否一遍又一遍地测试,没有暂停或清理过程?它只是让一切变得不同!
  • 我注意到private final List&lt;String&gt; list = new ArrayList&lt;String&gt;(); 可以替换为{System.out.println("wtf");} 哈哈

标签: java multithreading performance jvm


【解决方案1】:

清除(假)分享

由于内存中的布局,对象共享缓存行... 它已经解释了很多次(甚至在这个网站上):这里有一个good source 供进一步阅读。该问题适用于C#just as much(或C/C++)

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

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


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

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

【讨论】:

  • 这个应该很容易验证:把这行换成private final Object list = null;,看看效果是不是一样。 `
  • @Tom,没那么多 - 这只会添加 4 个字节,新的 ArrayList 会添加 10*4(or8) 个字节用于引用、4size、modcount 4 和 2 个对象标题。
  • 啊,是的 - 我在考虑对 SOTest 实例对齐的影响,但如果这些分配与列表实例散布。
  • 那样做。 8+8 易失多头(long l0, l2, l3....l7); volatile long id;volatile l8, l9, long la, lb, lc,... lf(你必须从前端和后端都填充)
  • @Gray,是的,循环已被优化并减少到 i+=ITERATIONS;
【解决方案2】:

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

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

证明:将 ArrayList 注释掉并放入:

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

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

【讨论】:

  • @assyalias "put instead in",或者换句话说替换ArrayList(该死的错字,not应该读now,一开始没看到,谢谢)
【解决方案3】:

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

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

【讨论】:

  • 更改数组列表的大小似乎没有什么区别。但是,该类仅包含对数组列表的引用,因此我认为更改其大小不会更改类的大小。
  • 这根本与ArrayList 无关。 new String("foo") 做同样的事情。
【解决方案4】:

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

您显然遇到了一些有趣的优化点。我怀疑在长 println 语句中添加 objects.get(0).id 正在消除围绕 id 字段使用的一些优化。除了++ 之外,id 没有其他用途,所以也许优化器正在优化一些对volatile id 的访问,从而提高了速度。只需使用 long x = objects.get(0).id; 访问 id 字段即可获得相同的性能提升。

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

【讨论】:

  • 当然@bestsss,但为什么添加(例如)new String("...") 会改变它?
  • 字符串在对象之后并包含额外的填充,包括。字符[]
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-02-21
  • 1970-01-01
  • 2015-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多