4

假设我创建了一个旨在模拟处理器内存的数组:

byte[] mem = new byte[0xF00];

该数组在仿真操作过程中使用,最终(读取:随频率)需要被丢弃或重置。我的问题是,哪个更快,为什么?

mem = new byte[0xF00];

或者:

for(int i = 0; i < mem.length; i++) mem[i] = 0;

它可能看起来并不重要,但在模拟大量处理器时,小的效率会产生影响。速度上的差异将来自 JVM 的垃圾收集;一方面,必须转储数组并收集垃圾,但是,JVM 不再需要分配(并且可能归零?)新内存。第二,避免了 JVM 开销,但我们仍然需要遍历数组中的每个元素。

作为对这个问题的额外警告:

  1. 成本比是否随数据类型的大小而变化?例如,ashort[]呢?
  2. 数组的长度会影响成本比吗?
  3. 最重要的是,为什么?
4

5 回答 5

5

您可以自己测试它,但删除和重新创建数组大致相同。

但是,它有两个缺点

  • 它会导致您的 CPU 数据缓存滚动,从而降低其有效性。
  • 它使触发 GC 的可能性更大,尤其是如果您经常这样做,这会暂停系统或减慢它的速度(如果它是并发的)

我更喜欢重用数组,不是因为它是最快的,而是它对应用程序的其余部分影响最小。


for (int size = 16; size <= 16* 1024; size *= 2) {
    int count1 = 0, count1b = 0,count2 = 0;
    long total1 = 0, total1b = 0, total2 = 0;
    for (long i = 0; i < 10000000000L; i += size) {
        long start = System.nanoTime();
        long[] longs = new long[size];
        if (longs[0] + longs[longs.length - 1] != 0)
            throw new AssertionError();
        long mid = System.nanoTime();
        long time1 = mid - start;
        Arrays.fill(longs, 1L);
        long time2 = System.nanoTime() - mid;
        count1b++;
        total1b += time1;
        if (time1 < 10e3) {// no GC
            total1 += time1;
            count1++;
        }
        if (time2 < 10e3) {// no GC
            total2 += time2;
            count2++;
        }
    }
    System.out.printf("%s KB took on average of %,d ns to allocate, %,d ns to allocate including GCs and %,d ns to fill%n",
            size * 8 / 1024.0, total1 / count1, total1b/count1b, total2 / count2);
}

印刷

0.125 KB took on average of 35 ns to allocate, 36 ns to allocate including GCs and 19 ns to fill
0.25 KB took on average of 39 ns to allocate, 40 ns to allocate including GCs and 31 ns to fill
0.5 KB took on average of 56 ns to allocate, 58 ns to allocate including GCs and 55 ns to fill
1.0 KB took on average of 75 ns to allocate, 77 ns to allocate including GCs and 117 ns to fill
2.0 KB took on average of 129 ns to allocate, 134 ns to allocate including GCs and 232 ns to fill
4.0 KB took on average of 242 ns to allocate, 248 ns to allocate including GCs and 368 ns to fill
8.0 KB took on average of 479 ns to allocate, 496 ns to allocate including GCs and 644 ns to fill
16.0 KB took on average of 1,018 ns to allocate, 1,055 ns to allocate including GCs and 1,189 ns to fill
32.0 KB took on average of 2,119 ns to allocate, 2,200 ns to allocate including GCs and 2,625 ns to fill
64.0 KB took on average of 4,419 ns to allocate, 4,604 ns to allocate including GCs and 4,728 ns to fill
128.0 KB took on average of 8,333 ns to allocate, 9,472 ns to allocate including GCs and 8,685 ns to fill

仅证明很难假设一种方法在所有情况下都比另一种方法快。

如果我把它改成long[]一个int[]我看到的差不多

0.125 KB took on average of 35 ns to allocate, 36 ns to allocate including GCs and 16 ns to fill
0.25 KB took on average of 40 ns to allocate, 41 ns to allocate including GCs and 24 ns to fill
0.5 KB took on average of 58 ns to allocate, 60 ns to allocate including GCs and 40 ns to fill
1.0 KB took on average of 86 ns to allocate, 87 ns to allocate including GCs and 94 ns to fill
2.0 KB took on average of 139 ns to allocate, 143 ns to allocate including GCs and 149 ns to fill
4.0 KB took on average of 256 ns to allocate, 262 ns to allocate including GCs and 206 ns to fill
8.0 KB took on average of 472 ns to allocate, 481 ns to allocate including GCs and 317 ns to fill
16.0 KB took on average of 981 ns to allocate, 999 ns to allocate including GCs and 516 ns to fill
32.0 KB took on average of 2,098 ns to allocate, 2,146 ns to allocate including GCs and 1,458 ns to fill
64.0 KB took on average of 4,312 ns to allocate, 4,445 ns to allocate including GCs and 4,028 ns to fill
128.0 KB took on average of 8,497 ns to allocate, 9,072 ns to allocate including GCs and 7,141 ns to fill
于 2013-04-23T06:15:24.477 回答
4

重新分配数组实际上并不会增加每次 GC 的成本,因为 GC 只访问和复制活动对象,而对死对象不做任何事情。但是,分配对象会导致次要 GC 更频繁地发生。但是,如果最近分配的对象都没有存活,那么次要 GC 的成本非常低,根本不会引起主要 GC。

此外,在当前的 Java 版本中,对象分配很便宜,分配空间的归零很容易被认为是 JVM 可能实现的最有效的归零。如果您设法像 JVM 一样快地将代码中的数组归零(编辑:正如 Steven Schlansker 所提到的,JIT 编译器可能会优化内存填充​​循环),那么重用数组应该会更快。无论如何,在您说明的 for 循环被 JIT 编译器优化之前,我认为它会慢很多。

要回答您的其他问题:

  • GC 会立即将分配空间(Eden)归零,因此无论是 ashort[]还是byte[]. short[]但是,当使用 a代替 a时,您的 for 循环只需要一半的迭代次数即可将相同数量的字节归零byte[](将字节或短设置为 0 应该没有任何区别)
  • 数组越长,for 循环需要的迭代次数就越多。所以这种增长是线性的。GC 还需要摊销线性时间来将所涉及的字节范围归零,因此我认为两种方法之间的比率保持不变。但是,可能存在比小内存区域清零更有效的方法,这将使 GC(一次整个分配空间)完成的清零比循环方法更有效。对于非常大的数组,情况可能会发生变化:它们直接分配到 Tenured 代(除非使用 G1),因此会导致更昂贵的主要 GC。
于 2013-04-23T08:15:51.033 回答
3

我同意继续使用数组对应用程序的影响最小的观察,但您的具体情况似乎对 GC 影响不大:

for(int i = 0; i < mem.length; i++) mem[i] = 0;

在上面的循环中(mem.length是 61440),会有2*61400赋值和61400比较。

现在,如果在特定对象的扫描或内存释放阶段发生 GC,整个内存块将被取消引用,这 IMO 应该比来自上述循环的统计数据更快。

但是,当代码/应用程序行为导致过多的 GC 周期(如果其频繁的主要周期甚至更糟)时,应用程序性能的 GC 实际成本就会出现。您的具体情况并没有显示更高的显式 GC。

我认为循环方法byte[]会更好。如果是这样Object[]的话,我们可能会有不同的方法。

于 2013-04-23T06:40:02.833 回答
1

我肯定会去做mem = new byte[0xF00];,让 GC 完成剩下的工作。

内存使用量可能会大一点,但它不会对您的应用程序产生影响,除非您每秒执行数千次。

执行时间会快很多,不需要手动调用 GC,反正他会做好自己的工作。

于 2013-04-23T06:15:02.730 回答
0

这里有 4 个重要的因素。

1) 目标平台是什么?(它有很多 RAM 吗?多个 CPU 内核?) 2) 您计划分配的最大内存量是多少?(更大的数量可能有利于分配/解除分配) 3)您计划使用哪个 JVM?4) 如果您的应用程序对性能至关重要,您为什么要使用 Java 开发它?

更重要的是,我会说,“不要担心过早的优化”。首先编写软件,然后对其进行分析,然后优化执行缓慢的部分。作为一般规则,算法性能通常是比数据结构性能更大的问题,特别是当您的数据结构基本上只是一个空白寻址空间时。

于 2013-04-23T06:51:47.507 回答