【问题标题】:array of structures, or structure of arrays?结构数组,还是数组结构?
【发布时间】:2017-02-01 13:03:25
【问题描述】:

嗯。我有一个表,它是我需要存储在 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>();

如果我最终使用大量 (> 106 ) 记录,其中偶尔访问单个记录,一次一个,我将如何弄清楚前面的方法(一个 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 是“时间”和“位置”,那么将这些值作为数组用于 MATLAB 对我来说很重要,这样我就可以有效地绘制/分析它们。

【问题讨论】:

  • 这究竟是如何优化的?你的意思是成员对齐?
  • 每个 Record 对象都会产生每个对象的存储成本(4 个字节?8 个字节?我不知道)和创建每个对象的性能成本。如果我有1000个,我不在乎。如果我有 100,000 或 1,000,000 个,我就会开始关心。

标签: java data-structures


【解决方案1】:

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

最初的问题非常有效,鉴于他的特殊情况,我倾向于同意第二种方法更好。我编写了图像处理代码,其中每个像素都需要一个数据结构,情况与此并无太大不同,只是我需要对每个像素进行频繁的随机访问。为每个像素创建一个对象的开销是巨大的。

【讨论】:

    【解决方案2】:

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

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

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

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

    【讨论】:

    • cletus -- 我非常尊重您的想法和意见,但是您给了我高级编程和软件设计的观点,这不是我想要的。在我能够直观地了解不同实现方式的成本和/或估算这些成本的能力之前,我无法学会忽略优化。
    • @Jason:我告诉了你在这种情况下你需要知道的关于优化的一切。第一个版本将调整一个数组的大小(这最终是一个 ArrayList 是什么)。第二个版本调整三个数组的大小并创建大量临时对象。它这样做并没有明显的好处(我可以看到)。你只需要看看。
    • @Jason S - 您应该忽略优化,直到您真正分析您的应用程序并发现真正的问题是什么。否则,您可能会浪费时间并降低代码的适应性和简单性。
    • @whaley:如果您将“您应该忽略优化”替换为“在大多数情况下您应该忽略优化”,我同意您的看法。就像我说的,我需要对事物的成本有一些直观的认识。
    • “过早的优化是万恶之源” (c) Donald Knuth
    【解决方案3】:

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

    • 内存使用:第一种方法使用更多内存,因为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 不带压缩 OOP 的 40 字节)。
    • 垃圾收集:虽然第二种方法似乎创建了许多对象(每次调用 getRecord 时创建一个),但可能并非如此,因为现代服务器 JVM(例如 Oracle 的 Java 7)可以应用逃逸分析和堆栈分配,以避免在某些情况下临时对象的堆分配;无论如何,GCing 短期对象很便宜。另一方面,如果没有数以百万计的长寿命对象(如第一种方法中那样)可访问性检查(或至少,这些对象可能使您的应用程序需要更加小心),则垃圾收集器可能更容易GC 生成大小的调整)。因此,第二种方法可能对 GC 性能更好。但是,要看看它在实际情况下是否有所作为,还是应该自己做一个标杆。
    • 序列化速度:在磁盘上(反)序列化大量基元的速度仅受硬盘速度的限制;序列化许多小对象不可避免地会变慢(尤其是如果您使用 Java 的默认序列化)。

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

    【讨论】:

      【解决方案4】:

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

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

      一个很好的例子是模拟什么时候使用单独的数组更方便,其中数值数据被打包在同一个数组中,以及其他属性(如名称、颜色等),这些属性仅用于在其他数组。

      【讨论】:

        【解决方案5】:

        我很好奇,所以我实际上运行了一个基准测试。如果您没有像现在这样[1] 重新创建对象,那么根据工作负载[2],SoA 会比 AoS 高 5-100%。在这里查看我的代码:

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

        [1] 我没有添加它是因为如果您对速度有足够的关注来考虑此重构,那么这样做会很愚蠢。

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

        【讨论】:

          【解决方案6】:

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

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

          【讨论】:

          • 如果您只对一个字段进行操作,则不会。
          • 为什么相同的内存使用渐近?对于第一种方法,一条记录 = 16 字节 + 每条记录的一些对象开销 + ArrayList 的一些开销。对于第二种方法,它是 16 字节 * 记录数 + OptimizedRecordStore 的一些开销。如果对象开销是 8 个字节,那么第一种方法的内存使用量大约增加了 50%……也许没关系,但我想弄清楚它是什么。
          【解决方案7】:

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

          【讨论】:

            【解决方案8】:

            我会选择第一种方法(结构数组)除非您访问存储的频率相对较低并且遇到严重的内存压力问题。

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

            第二个版本总体上使用更少的内存,但是在每次“get”时分配一个新对象是一个非常丑陋的解决方案,如果访问频繁,它将无法很好地执行。

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

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

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

            【讨论】:

              【解决方案9】:

              (不是直接的答案,而是我认为应该给出的答案)

              根据您的评论,

              "cletus -- 我非常尊重你的想法和意见,但是你给了我高级编程和软件设计的观点,这不是我想要的。我无法学会忽略优化,直到我能得到一个直观的了解不同实施方式的成本,和/或估算这些成本的能力。- Jason S 2009 年 7 月 14 日 14:27"

              您应该始终忽略优化,直到它出现问题。最重要的是让系统可供开发人员使用(这样他们就可以使其可供用户使用)。很少有时候你应该关注优化,事实上在大约 20 年的专业编码中,我总共关注过两次优化:

              1. 编写一个以比其他产品更快为主要目的的程序
              2. 编写智能手机应用程序以减少客户端和服务器之间发送的数据量

              在第一种情况下,我编写了一些代码,然后通过分析器运行它,当我想做某事但我不确定哪种方法最好(对于速度/内存)时,我会以一种方式编写代码并查看结果探查器,然后以另一种方式编码并查看结果。然后我会选择两者中更快的一个。这很有效,你会学到很多关于低级决策的知识。但是,我不允许它影响更高级别的课程。

              在第二种情况下,不涉及编程,但我做了同样的基本工作,即查看正在发送的数据并找出如何减少正在发送的消息数量以及正在发送的字节数。

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

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

              【讨论】:

              • +1——但我不同意“你应该总是忽略优化”——尤其是“总是”。我同意其余的观点,但请理解,有经验的程序员会根据这种经验做出大量无意识的决定,我们经验不足的程序员必须咕哝着,直到我们学会为止。在过去的 12 个月里,我有几个应用程序必须优化,因为它们根本不起作用——我正在处理一个每秒必须处理数百 KB 的系统,并且每次我使用“直到以后再优化”的方法我最终不得不重新设计我的代码。
              • 我同意无意识的部分......但我不同意在代码“完成”之前找出最快的方法应该是有意识的努力(大部分时间)。我最近做了一个新系统,我需要在 5 分钟内完成,我从大约 12 分钟开始,现在大约 3.5 分钟。在这个过程中,我一点一点地重写了 100% 的代码,直到速度很快。每次迭代也使事情变得更好。最后,我得到了一些与我预期完全不同的东西,而且也更好。
              • 另一种想法,你不会不遗余力地让事情变慢,例如当你不想重复时选择一个 List 而不是 Set (因此必须遍历 List插入之前),但这与担心数据表示等不同......如果您总是选择更简单的代码,然后找出它慢的地方,您将更容易在需要加速的地方加速它。
              【解决方案10】:

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

              【讨论】:

                【解决方案11】:

                我也会选择 ArrayList 版本,所以我不需要担心它的增长。你需要有一个像访问值这样的列吗?您的问题背后的场景是什么?

                编辑您也可以使用一个通用的long[][] 矩阵。 我不知道您如何将列传递给 Matlab,但我猜您在使用基于列的存储时并没有获得太多速度,更有可能是您在 java 计算中失去了速度。

                【讨论】:

                  猜你喜欢
                  • 2013-08-02
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-07-29
                  • 1970-01-01
                  • 2015-07-02
                  • 2012-02-25
                  • 2013-01-09
                  • 1970-01-01
                  相关资源
                  最近更新 更多