6

我正在运行此代码并得到意想不到的结果。我希望添加原语的循环执行得更快,但结果不一致。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        StringBuilder output = new StringBuilder();
        long start = System.currentTimeMillis();
        long limit = 1000000000; //10^9
        long value = 0;
        for(long i = 0; i < limit; ++i){}
        long i;
        output.append("Base time\n");
        output.append(System.currentTimeMillis() - start + "ms\n");
        start = System.currentTimeMillis();
        for(long j = 0; j < limit; ++j) {
            value = value + j;
        }
        output.append("Using longs\n");
        output.append(System.currentTimeMillis() - start + "ms\n");
        start = System.currentTimeMillis();
        value = 0;
        for(long k = 0; k < limit; ++k) {
            value = value + (new Long(k));
        }
        output.append("Using Longs\n");
        output.append(System.currentTimeMillis() - start + "ms\n");
        System.out.print(output);
    }
}

输出:

基准时间 359ms 使用 longs 1842ms 使用 Longs 614ms

我尝试在它自己的 java 程序中运行每个单独的测试,但结果是一样的。什么可能导致这种情况?

小细节:运行java 1.6

编辑:我让另外 2 个人试用此代码,其中一个得到了与我得到的完全相同的奇怪结果。另一个得到真正有意义的结果!我请那个获得正常结果的人给我们他的班级二进制文件。我们运行它,我们仍然得到奇怪的结果。问题不在编译时(我认为)。我正在运行 1.6.0_31,获得正常结果的人在 1.6.0_16,像我一样获得奇怪结果的人在 1.7.0_04。

编辑:在程序开始时使用 Thread.sleep(5000) 获得相同的结果。在整个程序中使用while循环也可以获得相同的结果(查看在java完全启动后时间是否会收敛到正常时间)

4

3 回答 3

3

我怀疑这是 JVM 预热效应。具体来说,代码在某个时候被 JIT 编译,这会扭曲您所看到的时间。

将整个批次放在一个循环中,并忽略报告的时间,直到它们稳定下来。(但请注意,它们不会完全稳定。垃圾正在生成,因此 GC 需要偶尔启动。这可能会扭曲时间,至少有点。处理这个问题的最好方法是运行外循环的大量迭代,并计算/显示平均次数。)

另一个问题是,某些 Java 版本上的 JIT 编译器可能会优化掉您尝试测试的内容:

  • 它可以计算出Long可以优化对象的创建和立即拆箱。(谢谢路易斯!)

  • 它可以找出循环正在做“忙碌的工作”......并完全优化它们。(value一旦每个循环结束,就不使用 的值。)


FWIW,一般建议你使用,Long.valueOf(long)而不是new Long(long)因为前者可以使用缓存Long实例。但是,在这种情况下,我们可以预测除了前几次循环迭代之外的所有循环中都会出现缓存未命中,因此该建议将无济于事。如果有的话,它可能会使有问题的循环变慢。


更新

我自己做了一些调查,结果如下:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        while (true) {
        test();
        }
    }

    private static void test() {
        long start = System.currentTimeMillis();
        long limit = 10000000; //10^9
        long value = 0;
        for(long i = 0; i < limit; ++i){}
        long t1 = System.currentTimeMillis() - start;
        start = System.currentTimeMillis();
        for(long j = 0; j < limit; ++j) {
            value = value + j;
        }
        long t2 = System.currentTimeMillis() - start;
        start = System.currentTimeMillis();
        for(long k = 0; k < limit; ++k) {
            value = value + (new Long(k));
        }
        long t3 = System.currentTimeMillis() - start;
        System.out.print(t1 + " " + t2 + " " + t3 + " " + value + "\n");
    }
}

这给了我以下输出。

28 58 2220 99999990000000
40 58 2182 99999990000000
36 49 157 99999990000000
34 51 157 99999990000000
37 49 158 99999990000000
33 52 158 99999990000000
33 50 159 99999990000000
33 54 159 99999990000000
35 52 159 99999990000000
33 52 159 99999990000000
31 50 157 99999990000000
34 51 156 99999990000000
33 50 159 99999990000000

请注意,前两列非常稳定,但第三列在第 3 次迭代中显示出显着的加速……可能表明 JIT 编译已经发生。

有趣的是,在我将测试分离到一个单独的方法之前,我没有看到第三次迭代的加速。这些数字看起来都像前两行。这似乎是说 JVM(我正在使用的)不会 JIT 编译当前正在执行的方法......或类似的东西。

无论如何,这表明(对我而言)应该有一个热身效果。如果您没有看到预热效果,那么您的基准测试正在做一些抑制 JIT 编译的事情......因此对于实际应用程序没有意义。

于 2012-05-27T05:27:23.580 回答
0

我也很惊讶。

我的第一个猜测是无意的“自动装箱”,但这显然不是您的示例代码中的问题。

此链接可能会提供线索:

http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/Long.html

public static Long valueOf(long l)

返回一个 Long 实例,表示指定的 long 值。如果不需要新的 Long 实例,则通常应优先使用此方法而不是构造函数 Long(long),因为此方法可能会通过缓存频繁请求的值来显着提高空间和时间性能。

Parameters:
    l - a long value. 

Returns:
    a Long instance representing l.

Since:
    1.5

但是,是的,我希望使用包装器(例如“Long”)花费更多时间和更多空间。我不希望使用包装器会三倍!

==================================================== ===============================

附录:

我用你的代码得到了这些结果:

Base time     6878ms
Using longs  10515ms
Using Longs 428022ms

我在 pokey 32 位单核 CPU 上运行 JDK 1.6.0_16。

于 2012-05-27T05:20:07.790 回答
0

好的 - 这是一个稍微不同的版本,以及我的结果(运行 JDK 1.6.0_16 pokey 32 位单代码 CPU):

import java.util.*;

/*
Test      Base   longs  Longs/new  Longs/valueOf
----      ----   -----  ---------  -------------
   0       343     896       3431           6025
   1       342     957       3401           5796
   2       342     881       3379           5742
*/

public class LongTest {

    private static int limit = 100000000;
    private static int ntimes = 3;
    private static final long[] base = new long[ntimes];
    private static final long[] primitives = new long[ntimes];
    private static final long[] wrappers1 = new long[ntimes];
    private static final long[] wrappers2 = new long[ntimes];

    private static void test_base (int idx) {
       long start = System.currentTimeMillis();
       for (int i = 0; i < limit; ++i){}
       base[idx] = System.currentTimeMillis() - start;
    }

    private static void test_primitive (int idx) {
       long value = 0;
       long start = System.currentTimeMillis();
       for (int i = 0; i < limit; ++i){
         value = value + i;
       }
       primitives[idx] = System.currentTimeMillis() - start;
    }

    private static void test_wrappers1 (int idx) {
       long value = 0;
       long start = System.currentTimeMillis();
       for (int i = 0; i < limit; ++i){
         value = value + new Long(i);
       }
       wrappers1[idx] = System.currentTimeMillis() - start;
    }

    private static void test_wrappers2 (int idx) {
       long value = 0;
       long start = System.currentTimeMillis();
       for (int i = 0; i < limit; ++i){
         value = value + Long.valueOf(i);
       }
       wrappers2[idx] = System.currentTimeMillis() - start;
    }

    public static void main(String[] args) {

      for (int i=0; i < ntimes; i++) {
         test_base (i);
            test_primitive(i);
            test_wrappers1 (i);
            test_wrappers2 (i);
      }

      System.out.println ("Test      Base   longs  Longs/new  Longs/valueOf");
      System.out.println ("----      ----   -----  ---------  -------------");
      for (int i=0; i < ntimes; i++) {
         System.out.printf ("  %2d    %6d  %6d     %6d         %6d\n",
            i, base[i], primitives[i], wrappers1[i], wrappers2[i]);
       }
    }
}

==================================================== ======================

2012 年 5 月 28 日:

以下是一些额外的时间,来自运行 Windows 7/64 并运行相同 JDK 修订版 1.6.0_16 的更快(但仍然适中)的双核 CPU:

/*
PC 1: limit = 100,000,000, ntimes = 3, JDK 1.6.0_16 (32-bit):
Test      Base   longs  Longs/new  Longs/valueOf
----      ----   -----  ---------  -------------
   0       343     896       3431           6025
   1       342     957       3401           5796
   2       342     881       3379           5742

PC 2: limit = 1,000,000,000, ntimes = 5,JDK 1.6.0_16 (64-bit):
Test      Base   longs  Longs/new  Longs/valueOf
----      ----   -----  ---------  -------------
   0         3       2       5627           5573
   1         0       0       5494           5537
   2         0       0       5475           5530
   3         0       0       5477           5505
   4         0       0       5487           5508

PC 2: "for loop" counters => long; limit = 10,000,000,000, ntimes = 5:
Test      Base   longs  Longs/new  Longs/valueOf
----      ----   -----  ---------  -------------
   0      6278    6302      53713          54064
   1      6273    6286      53547          53999
   2      6273    6294      53606          53986
   3      6274    6325      53593          53938
   4      6274    6279      53566          53974
*/

你会注意到:

  1. 我没有使用 StringBuilder,我将所有的 I/O 分离出来,直到程序结束。

  2. “long”原语始终等同于“no-op”

  3. “长”包装器始终慢得多

  4. "new Long()" 比 "Long.valueOf()" 稍快

  5. 将循环计数器从“int”更改为“long”会使前两列(“base”和“longs”慢得多

  6. 在前几次迭代之后,“JIT 预热”可以忽略不计......

  7. ... provided I/O (like System.out) and potentially memory-intensive activities (like StringBuilder) are moved outside of the actual test sections.

于 2012-05-27T06:20:35.157 回答