结构数组或数组结构?

时间:2009-07-14 14:07:59

标签: java data-structures

嗯。我有一个表,这是一个我需要用Java存储的结构数组。天真的不用担心内存的方法说这样做:

public class Record {
  final private int field1;
  final private int field2;
  final private long field3;
  /* constructor & accessors here */
}

List<Record> records = new ArrayList<Record>();

如果我最终使用大量(> 10 6 )记录,偶尔访问个别记录,一次一个,我将如何弄清楚前面的方法(一个ArrayList)将与优化的存储成本方法进行比较:

public class OptimizedRecordStore {
  final private int[] field1;
  final private int[] field2;
  final private long[] field3;

  Record getRecord(int i) { return new Record(field1[i],field2[i],field3[i]); }
  /* constructor and other accessors & methods */
}

修改

  • 假设记录数是不经常更改或从不
  • 我可能不会使用OptimizedRecordStore方法,但我想了解存储成本问题,以便我可以放心地做出这个决定。
  • 显然,如果我在上面的OptimizedRecordStore方法中添加/更改记录数,我要么必须用新的对象替换整个对象,要么删除“final”关键字。
  • kd304提出了我心中的一个好点。在与此类似的其他情况下,我需要对记录进行列访问,例如如果field1和field2是“time”和“position”,那么将这些值作为数组用于MATLAB非常重要,因此我可以有效地绘制/分析它们。

11 个答案:

答案 0 :(得分:7)

在这种情况下,给出一般“在必要时进行优化”的答案是无益的,因为恕我直言,程序员应该始终意识到设计选择中不同的性能,当这种选择导致一个数量级的性能损失时,特别是API编写者。

原始问题非常有效,我倾向于同意第二种方法更好,因为他的具体情况。我已经编写了图像处理代码,其中每个像素需要一个数据结构,这种情况与此不太相似,除了我需要频繁随机访问每个像素。为每个像素创建一个对象的开销是巨大的。

答案 1 :(得分:5)

第二个版本更多,更糟糕。在执行插入或删除操作时,您将调整三个数组的大小,而不是调整一个数组的大小。更重要的是,第二个版本将导致创建更多临时对象,并且它将在访问时执行。这可能导致大量垃圾(从GC的角度来看)。不好。

一般来说,在考虑性能之前,您应该担心如何使用这些对象。所以你有一个包含三个字段或三个数组的记录。哪一个更准确地描绘了您的建模?我的意思是,当你插入或删除一个项目时,你是在做三个数组中的一个,还是将这三个数组作为一个块?

我怀疑是后者,在这种情况下,前者更有意义。

如果您真的关心插入/删除性能,那么可能需要使用不同的数据结构,可能是SortedSet或Map或SortedMap。

答案 2 :(得分:5)

如果您有数百万条记录,第二种方法有几个优点:

  • 内存使用:第一种方法使用更多内存,因为 a)堆中的每个Java对象都有一个标头(包含类ID,锁状态等); b)对象在内存中对齐; c)对对象的每个引用花费4个字节(在具有压缩OOP或32位JVM的64位JVM上)或8个字节(没有压缩OOP的64位JVM)。见e。 G。 CompressedOops了解更多详情。所以第一种方法需要大约两倍的内存(更确切地说:根据我的基准测试,一个有16字节有效负载的对象+对它的引用在32位Java 7上占用28字节,在64位Java 7上占用36字节压缩OOP,在64位Java 7 w / o压缩OOP上40字节。)
  • 垃圾收集:虽然第二种方法似乎创建了许多对象(每次调用getRecord时都有一个),但它可能不是这样,因为现代服务器JVM(例如Oracle的Java 7) )可以应用转义分析和堆栈分配,以避免在某些情况下临时对象的堆分配;无论如何,GCing短期物品很便宜。另一方面,如果没有数百万个长期存在的对象(如第一种方法中那样)可检查的可达性(或者至少这些对象可能会使您的应用程序需要更加小心),垃圾收集器可能更容易调整GC生成大小)。因此,第二种方法可能更适合GC性能。但是,要了解它是否会对实际情况产生影响,我们应该自己做一个基准。
  • 序列化速度:(de)序列化磁盘上的大量基元的速度仅受HDD速度的限制;序列化许多小对象不可避免地会变慢(特别是如果你使用Java的默认序列化)。

因此,我经常对非常大的集合使用第二种方法。但是,当然,如果你有足够的内存并且不关心序列化,那么第一种方法就更简单了。

答案 3 :(得分:3)

您打算如何访问数据?如果对字段的访问总是耦合,那么使用第一个选项,如果您要自己处理字段,那么第二个选项更好。

请参阅维基百科中的这篇文章:Parallel Array

关于何时使用单独数组更方便的一个很好的例子可能是模拟,其中数值数据被打包在同一个数组中,以及其他属性,如名称,颜色等,只是为了呈现数据而被访问其他阵列。

答案 4 :(得分:3)

我很好奇所以我实际上运行了基准测试。如果你没有像[1]那样重新创建对象,那么SoA将AoS击败5-100%,具体取决于工作负载[2]。请在此处查看我的代码:

https://gist.github.com/twolfe18/8168262c5420c7a62d39

[1]我没有补充说,因为如果你对速度有足够的考虑来考虑这个重构,那么这样做会很愚蠢。

[2]这也不考虑重新分配,但同样,这通常是你可以摊还或静态知道的东西。这是纯速度基准的合理假设。

答案 5 :(得分:2)

请注意,第二种方法可能会对缓存行为产生负面影响。如果您想一次访问一条记录,最好不要将该记录分散到整个地方。

此外,您在第二种方法中赢得的唯一记忆是(可能)由于成员对齐。 (并且必须分配一个单独的对象)。 否则,它们具有完全相同的内存使用,渐近。由于地方性,IMO

,第一种选择要好得多

答案 6 :(得分:2)

每当我尝试在Java中进行数字运算时,我总是不得不恢复到C风格的编码(即接近你的选项2)。它最大限度地减少了系统中浮动的对象数量,而不是1,000,000个对象,只有3个。我能够使用C风格对实时声音数据进行一些FFT分析,它也是如此使用对象很慢。

答案 7 :(得分:2)

我会选择第一种方法(结构数组),除非您相对不经常访问商店并且遇到严重的内存压力问题。

第一个版本基本上以“自然”形式存储对象(+1 BTW用于使用不可变记录)。由于每个对象的开销(可能大约8-16个字节,具体取决于您的JVM),这会占用更多的内存,但是只需一个简单的步骤就可以方便且易于理解的形式访问和返回对象。

第二个版本整体使用较少的内存,但是在每个“get”上分配一个新对象是一个相当丑陋的解决方案,如果频繁访问则效果不佳。

要考虑的其他一些可能性:

一个有趣的“极端”变体是采用第二个版本,但编写您的算法/访问方法以直接与底层数组交互。这显然会导致复杂的相互依赖关系和一些丑陋的代码,但如果你真的需要它可能会给你绝对最好的性能。将此方法用于密集图形应用程序(例如操纵大量3D坐标)是很常见的。

“混合”选项是将基础数据存储在数组结构中,如第二个版本中所示,但是将访问的对象缓存在HashMap中,以便您只在第一次访问特定索引时生成对象。如果只有一小部分对象可能被访问,那么可能有意义,但所有数据都需要“以防万一”。

答案 8 :(得分:2)

(不是直接回答,但我认为应该给出一个)

根据您的评论,

“cletus - 我非常尊重你的想法和意见,但你给了我高级编程和软件设计观点,这不是我正在寻找的。我无法学会忽视优化,直到我得到一个直观感知不同实施风格的成本,和/或估计这些成本的能力。 - Jason S 2009年7月14日14:27“

您应该始终忽略优化,直到它出现问题为止。最重要的是让系统可供开发人员使用(因此他们可以让用户使用它)。很少有人会关注自己的优化,事实上在大约20年的专业编码中,我总是关心优化两次:

  1. 编写一个主要目的是比另一个产品更快的程序
  2. 编写智能手机应用程序,旨在减少客户端和服务器之间发送的数据量
  3. 在第一种情况下,我编写了一些代码,然后通过分析器运行它,当我想做某事时我不确定哪种方法最好(对于速度/内存)我会编写一种方式并查看结果探查器,然后以另一种方式编码并查看结果。然后我会选择两者中较快的一个。这有效,你可以学到很多关于低级别决策的知识。但是,我没有让它影响更高级别的课程。

    在第二种情况下,没有涉及编程,但我做了相同的基本操作,查看正在发送的数据,并弄清楚如何减少发送的消息数量以及发送的字节数。

    如果您的代码清晰,那么一旦发现它很慢就会更容易加速。正如Cletus在他的回答中所说的,你正在调整一次-vs-三次......一次将比三次更快。从更高的角度来看,一次比三次更容易理解,因此更可能是正确的。

    就个人而言,我宁愿慢慢得到正确的答案,然后快速得到错误的答案。一旦我知道如何得到正确答案,那么我就可以找到系统速度慢的地方,并用更快的实现来替换它的那些部分。

答案 9 :(得分:1)

我也会选择ArrayList版本,所以我不需要担心增长它。您是否需要一个像访问值的列?您的问题背后的情况是什么?

修改您还可以使用通用的long[][]矩阵。 我不知道你是如何将列传递给Matlab的,但我猜你的基于列的存储速度并不快,你更有可能在java计算中失去速度。

答案 10 :(得分:1)

因为你正在使int []字段成为最终的,所以你只能对数组进行一次初始化,就是这样。因此,如果你想要10 ^ 6 field1,那么Java需要为每个int []分隔那么多内存,因为你无法重新分配这些数组的大小。使用ArrayList,如果您事先不知道记录数并且可能会删除记录,则可以预先节省大量空间,然后在删除记录时也可以保存。