【问题标题】:How much should I add when resizing an array?调整数组大小时我应该添加多少?
【发布时间】:2009-12-02 20:10:36
【问题描述】:

我正在与另一名学生进行竞赛,以完成我们家庭作业的最快版本,出于性能原因,我没有使用 ArrayList(我自己调整数组的大小将基准时间从 56 秒缩短到 4 秒),但是我想知道每次需要时我应该调整多少数组。具体来说,我的代码的相关部分是:

private Node[] list;
private int size; // The number of items in the list
private static final int N; // How much to resize the list by every time

public MyClass(){
  list = new Node[N];
}

public void add(Node newNode){
  if(size == list.length){
    list = Arrays.copyOf(list, size + N);
  }
  list[size] = newNode;
  size++;
}

TL;DR:我应该做什么N

【问题讨论】:

  • new 是合法的变量名(在 add() 声明中)??
  • 糟糕。我的意思是那是 newNode。

标签: java arrays optimization


【解决方案1】:

建议在调整大小时将数组大小加倍。将大小加倍会导致摊销的线性时间成本。

天真的想法是有两个与调整大小相关的成本:

  • 复制性能成本 - 将元素从先前数组复制到新数组的成本,以及
  • 内存开销成本 - 分配的未使用内存的成本。

如果您通过一次添加一个元素来重新调整数组的大小,则内存开销为零,但复制成本变为二次方。如果分配太多槽,复制成本将是线性的,但内存开销太大。

加倍导致线性摊销成本(即在很长一段时间内,复制成本与数组大小成线性关系),并且保证不会浪费超过一半的数组。

更新:顺便说一下,显然 Java 的 ArrayList 扩展了 (3/2)。这使得它在内存上更加保守,但在复制方面成本更高。为您的使用进行基准测试不会受到伤害。

次要更正:加倍会使调整大小的成本线性摊销,但会确保您有一个摊销的常数时间插入。检查CMU's lecture on Amortized Analysis

【讨论】:

  • 我在代码中使用了将大小翻倍的想法并对其进行了基准测试,结果 new ArrayList(1000) 比我的初始大小为 1000 的代码慢了大约 100 倍;
  • 加倍防止使用先前分配的空间,如果该空间仍然可用。 phi 因子定义了允许这种重用的增长因子的上限。在第一次适合的分配器不存在争用的情况下,phi 的增长率只需要在 每隔一次 重新分配时分配更多空间。
【解决方案2】:

3/2 很可能被选为“划分干净但小于phi 的东西”。早在 2003 年 11 月就有 an epic thread on comp.lang.c++.moderated 探讨 phi 如何在重新分配期间为首次适配分配器建立重用先前分配的存储的上限。

请参阅post #7 from Andrew Koenig 第一次提到phi 对这个问题的应用。

【讨论】:

    【解决方案3】:

    如果您大致知道将有多少项,则将数组或 ArrayList 预先分配到该大小,您将永远不必扩展。无与伦比的性能!

    如果做不到这一点,实现良好摊销成本的合理方法是保持一定百分比的增长。 100% 或 50% 很常见。

    【讨论】:

      【解决方案4】:

      您应该将列表的大小调整为之前大小的倍数,而不是每次都添加一个常数。

      例如:

      newSize = oldSize * 2;
      

      不是

      newSize = oldSize + N;
      

      【讨论】:

        【解决方案5】:

        每次需要调整大小时将大小加倍,除非您知道或多或少是最好的。

        如果内存不是问题,只需从一个大数组开始。

        【讨论】:

        • 问题是内存不是问题,但我正在读取一个任意大的文件。
        • 我也要把这个上交,所以 list = new Node[Integer.MAX_VALUE] 可能会让老师不高兴。
        • 这也可能比系统拥有的内存更多。我会从更适度的开始,比如 1024 或 2048。
        【解决方案6】:

        您的代码似乎与 ArrayList 所做的差不多 - 如果您知道您将使用一个大列表,您可以在创建列表时将其传递一个初始大小并完全避免调整大小。这当然假设您要追求原始速度并且内存消耗不是问题。

        【讨论】:

        • 我用 N = 1000 尝试了我的代码;与新的 ArrayList(1000);我的速度快了约 100 倍。不过好主意,我没想过要设置初始大小。
        • 看起来很奇怪,但我怀疑 ArrayList 可能有一些健全性检查会减慢它的速度。
        • @Brendan 您的基准测试结果似乎非常奇怪。查看 ArrayList 的源代码,至少在我的 openjdk 1.6.0 上,它完全符合您的要求;给出或采用少数算术运算来计算新容量(与复制数组的成本相比可以忽略不计)。
        • 这是我的测试脚本:pastebin.com/m536bb968 还有我的数组类:pastebin.com/m75f34b75 在我的计算机上,ArrayList 用了大约 2.5 秒,而我的数组用了 0.2 秒。我真的不知道为什么.. 我使用的是 Sun JDK。
        • 我也尝试将第二个更改为list = Arrays.copyOf(list, size * 3/2);,但没有任何区别。
        【解决方案7】:

        来自其中一个答案的cmets:

        问题是内存不是 问题,但我正在阅读任意 大文件。

        试试这个:

        new ArrayList<Node>((int)file.length());
        

        你也可以用你的数组来做。那么在这两种情况下都不应该调整大小,因为数组将是文件的大小(假设文件不再是 int...)。

        【讨论】:

          【解决方案8】:

          为了获得最佳性能,您需要尽可能少地调整大小。将初始大小设置为您通常需要的大小,而不是从 N 个元素开始。在这种情况下,您为 N 选择的值将不那么重要。


          如果您要创建大量不同大小的列表对象,那么您需要使用基于池的分配器,并且在退出之前不要释放内存。

          为了完全消除复制操作,您可以使用数组列表

          【讨论】:

            【解决方案9】:

            这是一个类比,很久以前,当我在大型机上工作时,我们使用了一个名为 VSAM 的文件系统,它需要您指定初始文件大小和所需的可用空间量。

            只要可用空间量降至所需阈值以下,则所需的可用空间量将在后台分配,同时程序继续处理。

            看看这是否可以在 java 中使用一个单独的线程来分配额外的空间并在主线程继续处理时将其“附加”到数组的末尾会很有趣。

            【讨论】:

            • 我严重怀疑 Java 会给你那么多控制权。我能做的最好的事情是创建一个新数组,并希望 Java 的数组副本复制一段内存,而不仅仅是一个 for 循环.. :)
            猜你喜欢
            • 2023-04-05
            • 1970-01-01
            • 2011-11-08
            • 2015-08-06
            • 2018-11-26
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多