【问题标题】:Java Performance/Memory Consumption: Class vs. ArrayJava 性能/内存消耗:类与数组
【发布时间】:2016-01-05 09:55:25
【问题描述】:

出于兴趣:最近,我在我的一个 Java 项目中遇到了一种情况,我可以将一些数据存储在二维数组中,或者为其创建一个专用类,我会将其实例放入一维数组中.所以我想知道在性能(运行时、内存消耗)方面是否存在一些关于这个主题的规范设计建议?

不考虑设计模式(极其简化的情况),假设我可以像这样存储数据

class MyContainer {
  public double a;
  public double b;
  ...
}

然后

MyContainer[] myArray = new MyContainer[10000];
for(int i = myArray.length; (--i) >= 0;) {
  myArray[i] = new MyContainer();
}
...

double[][] myData = new double[10000][2];  
...

我以某种方式认为基于数组的方法应该更紧凑(内存)和更快(访问)。再说一次,也许不是,数组也是对象,数组访问需要检查索引,而对象成员访问不需要。(?)对象数组的分配可能(?)需要更长的时间,因为我需要迭代地创建由于额外的类,实例和我的代码会更大。

因此,我想知道常见 JVM 的设计是否在访问速度和内存消耗方面为一种方法提供了优于另一种方法的优势?

非常感谢。

【问题讨论】:

  • 澄清一下,Java 数组 Object(s)。甚至是基元数组。
  • @ElliotFrisch:是的,我知道这一点(参见...数组也是对象...)。仍然:可能是 JVM/JIT 以一种特殊的方式处理数组,这可以使它们以一种或另一种方式更快地访问。还可能有缓存/内存布局等...因此我还询问了...常见JVM的设计....
  • 您可能会看看是否可以使用enum。这些被特殊对待,因为它们是编译时构造。

标签: java arrays performance class memory-consumption


【解决方案1】:

再一次,也许不是,数组也是对象

没错。所以我认为这种方法不会给你带来任何东西。

如果你想沿着这条路走下去,你可以把它展平成一个一维数组(你的每个“对象”然后占用两个槽)。这将使您可以立即访问所有对象中的所有字段,而无需遵循指针,并且整个事情只是一个大内存分配:由于您的组件类型是原始的,就内存分配而言只有一个对象(容器数组本身)。

这是人们 wanting to have structs and value types in Java 的动机之一,类似的考虑推动了专门的高性能数据结构库的开发(摆脱了不必要的对象包装器)。

不过,在您真正拥有庞大的数据结构之前,我不会担心它。只有这样,面向对象方式的开销才有意义。

【讨论】:

  • 确实,这将是最节省内存的方法。在速度方面,会做一些像myarray[(i<<1)+j] 这样的事情,在我们的例子中,i 将是索引,j 将是01,比myarray[i][j] 更快? (我有点认为“是”,但同样,我不确定在常见的 JVM/JIT 中做了什么样的优化。)
  • 是的,这就是我所说的“必须遵循指示”的意思。每个组件对象都是不同内存位置中的单独对象。平面阵列要快得多。
【解决方案2】:

我不知何故认为基于数组的方法应该更紧凑(内存)和更快(访问)

不会的。您可以使用 Java 管理界面轻松确认这一点:

com.sun.management.ThreadMXBean b = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
long selfId = Thread.currentThread().getId();
long memoryBefore = b.getThreadAllocatedBytes(selfId);

// <-- Put measured code here

long memoryAfter = b.getThreadAllocatedBytes(selfId);
System.out.println(memoryAfter - memoryBefore);

在测量代码下放入new double[0]new Object(),您会看到这些分配将需要完全相同的内存量。

可能是 JVM/JIT 以一种特殊的方式处理数组,这可以使它们以一种或另一种方式更快地访问。

JIT 做一些vectorization of an array operations if for-loops。但它更多的是关于算术运算的速度而不是访问速度。除此之外,什么都想不出来。

【讨论】:

  • 感谢您澄清这一点。所以我们可以预期对象和二维数组的总内存消耗应该是相同的。知道这一点很好。关于“紧凑性”,我还可以想到一件事:如果我分配,比如new double[1000][2],我希望这应该成为堆上的一块连续内存。但是,如果我执行for(int i = 0; i&lt;1000; i++) { data[i] = new MyObject(); },我不确定,有些对象可能会完全落在堆的不同位置(?)。所以这可能是对对象数组想法的惩罚(?)
  • 关于内存布局也应该没有区别。 JVM 从称为 TLAB (blogs.oracle.com/jonthecollector/entry/the_real_thing) 的线程本地缓冲区分配内存。对象在内存中是连续的,无论它们的类型如何,只要它们完全适合 TLAB。
【解决方案3】:

我在这种情况下看到的规范建议是,过早优化是万恶之源。遵循这意味着您应该坚持使用最容易编写/维护/通过代码质量制度的代码,然后如果您有可衡量的性能问题,请考虑优化。

在您的示例中,内存消耗是相似的,因为在对象情况下,您有 10,000 个引用加上每个引用的两个双精度值,而在二维数组情况下,您有 10,000 个引用(第一个维度)到每个包含两个双精度值的小数组。所以两者都是一个基础参考加上 10,000 个参考加上 20,000 个双精度。

更有效的表示是两个数组,其中有两个基引用加上 20,000 个双精度数。

double[] a = new double[10000];
double[] b = new double[10000];

【讨论】:

  • 这个非常容易维护,容易理解,而且不需要太多内存。但是,它可能会引入性能问题,因为它会“撕裂”数据元组/对。如果我有double[10000][2],那么每个子数组的 2 个元素将在内存中彼此相邻,这对缓存很有用。在两个单个数组中(如果我正确理解您的建议),那么每个元素将落在不同的数组中。元组的两个元素在内存中将是 10000 的两倍。这可能会导致更多的缓存未命中并降低处理速度。
  • @ThomasWeise,这在技术上是正确的,但结论是错误的。实际上,您有多个高速缓存行,并且在实践中处理一个连续的内存块或两个彼此分开的连续内存块并不重要。建议的方法是完全可行的。但是,如果您真的想压缩它,请将两个数组打包成一个交错的数组。
猜你喜欢
  • 2012-06-03
  • 1970-01-01
  • 2017-02-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-27
  • 2011-03-11
相关资源
最近更新 更多