为什么Arrays.copyOf这么慢?

时间:2018-12-27 13:06:42

标签: java

函数ArrayList.add()的运行速度非常快。我检查了源代码,发现工具是Arrays.copyOf()

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

但是当我在代码中使用方法Arrays.copyOf()时,它变得非常慢。您只需运行以下代码即可查看它:

public class TestArrayCopy {
  public static void main(String[] args) {
    long t = System.currentTimeMillis();
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
      list.add(i, i);
    }
    System.out.println(System.currentTimeMillis() - t);

    t = System.currentTimeMillis();
    Integer[] array = new Integer[0];
    for (int i = 0; i < 100000; i++) {
      array = Arrays.copyOf(array, array.length +1);
      array[i] = i;
    }
    System.out.println(System.currentTimeMillis() - t);
  }
}

有什么想法吗?

5 个答案:

答案 0 :(得分:3)

如果您要问两个循环为什么运行时间不同(第二个循环慢得多),则原因是大多数对list.add(i, i)的调用都不需要重新创建后备数组。

仅当后备阵列已满时,才会创建更大的阵列。而且较大的数组比原始数组大50%,因此它可以在add变满之前处理许多后续对import serial from tkinter import * import tkinter as ttk from tkinter import * top = Tk() top.title("Tk dropdown example") # Add a grid mainframe = Frame(top) mainframe.grid(column=10,row=10, sticky=(N,W,E,S) ) mainframe.columnconfigure(0, weight = 1) mainframe.rowconfigure(0, weight = 1) mainframe.pack(pady = 100, padx = 100) # Create a Tkinter variable tkvar = StringVar(top) # Dictionary with options choices = { '9600','19200','34800'} tkvar.set('9600') # set the default option popupMenu = OptionMenu(mainframe, tkvar, *choices) Label(mainframe, text="Baudrate").grid(row = 1, column = 1) popupMenu.grid(row = 2, column =1) ard = serial.Serial('COM4', timeout=1) ard.baudrate = drop k = ard.readline().decode('ascii') print(k) # on change dropdown value def change_dropdown(*args): global drop; drop = int(tkvar.get()); #ard.baudrate = drop; print( drop ) return drop # link function to change dropdown tkvar.trace('w', change_dropdown) top.mainloop() 的调用。

答案 1 :(得分:3)

您的代码每次添加新元素时都会调用copyOf()

  • 第一次,不需要复制任何元素,因为原始数组的长度为0。
  • 第二次必须复制一个元素。
  • 第三次,两个要素。
  • 第四次,三个要素。
  • ...等等。

因此,对于添加的每个元素,您都必须花更多的精力来复制以前的元素。因此,如果要添加n个元素,则必须对单个元素执行总共1 + 2 + 3 + ... + (n - 1) = n * (n - 1) / 2 = n^2 / 2 - n / 2个复制。因此,您的运行时与所添加元素数量的 square 成正比。

将此与适当的方法进行对比,即拥有比您所需的数组更大的数组(这使您有足够的空间添加更多元素而无需始终复制),并将大小乘以 每次需要扩展时固定的因子。这要求您分别跟踪已添加的元素数,并向用户撒谎以了解您的真实尺寸。该因子通常小于2(Java代码使用1.5:int newCapacity = oldCapacity + (oldCapacity >> 1);),但是如果使用2:

  • 初始数组大小是一些小但非零的数字,例如4,因此您可以免费添加4个元素(嗯,分配和初始化数组的成本实际上是4)
  • 对于第五个元素,将大小翻倍至8,然后复制4个旧元素;您现在可以再添加4个
  • 对于第九个元素,将大小翻倍至16,然后复制8个旧元素;您现在可以再添加8个
  • 依此类推:对于第 n + 1 个元素,将大小加倍为 2 * n ,然后复制旧的 n 元素,这为您提供了 n 个更多元素的空间。

即使不评估复制的总和,我们也可以看到,每批 n 个新元素都已经被以前的 n 的复制“支付”了元素,因此复制工作是 linear 而不是二次的。实际上,4 + 4 + 8 + 16 + ... + n / 2 + n = 2 * n(如果n是2的幂)。

答案 2 :(得分:0)

因为添加新元素时并非总是调用grow()ArrayList总是增加50%。

相关行如下:

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);

(oldCapacity >> 1)向右移一点,因此除以2。这意味着newCapacityoldCapacity的+ 50%。仅在超出此限制时,才会下一次调用grow()

答案 3 :(得分:0)

Arrays.copyOf与预期的一样慢。尽管List.add()的速度与预期的一样快,但是由于Arrays.copyOf方法中的grow不会在每次调用add方法时发生。 grow仅在ArrayList的功能不足时才会发生。

答案 4 :(得分:0)

区别是amortized runtime。每次创建新副本时,您的实现都会复制整个阵列。每个复制操作都是O(n),因此要花费整个操作的费用

1 + 2 + ... + n = n * (n + 1) / 2 = O(n^2)
另一方面,

grow以不同的方式增加大小。在此行中计算出数组的新大小:

int newCapacity = oldCapacity + (oldCapacity >> 1);

本质上可以归结为

int newCapacity = (int) (oldCapacity * 1.5)

所以一旦数组被填满,数组就会增长1.5倍

相比之下,这需要插入n和初始大小为m

m + 1.5 * m + 1.5 ^ 2 * m + ... + n = 
m + 1.5 * m + 1.5 ^ 2 * m + ... + 1.5 ^ log_1.5(n / m) * m = 
m * (1 - 1.5 ^ (log_1.5(n / m) + 1) / (1 - 1.5) = 
m * 2 * (n / m * 1.5 - 1) =
3 * n - 2 * m

摊销的运行时间可以计算为n插入的总成本除以n

(3 * n - 2 * m) / n = 3 - 2 * m / n

由于在您的情况下m < n的第二部分变得可以忽略不计,所以我们最终将O(1)作为摊销运行时以及所有O(n)插入的运行时与O(n^2)不同之处在于每次插入后都复制整个数组。