考虑以下Java代码(完整,可编译且运行良好)。
该代码创建一个包含5,000,000个整数(1到5百万)的数组,在其上循环,并创建找到的理想平方的ArrayList。使用天真的技术(而不是位操作)可以检测出完美的平方,但这并不是当前问题的重点。
数学上,介于1到5M之间,有2236个完美正方形。因此,要放入完美正方形的ArrayList的最终大小为2236。
import java.util.ArrayList;
public class PerfSquares {
public static ArrayList perfectSquares(int[] arr) {
ArrayList al = new ArrayList();
// ArrayList al = new ArrayList(arr.length);
for (int i = 0; i < arr.length; i++) {
double root = Math.sqrt(arr[i]);
int irt = (int) Math.floor(root);
if (irt * irt == arr[i]) {
al.add(arr[i]);
}
}
return al;
}
public static void main(String[] args) {
int[] arr = new int[5000000];
for (int i = 0; i < arr.length; i++) {
arr[i] = i + 1;
}
long s = System.currentTimeMillis();
perfectSquares(arr);
long e = System.currentTimeMillis();
System.out.println(e - s);
}
}
我想重点介绍ArrayList的声明。这两行,其中之一被注释掉:
ArrayList al = new ArrayList();
//ArrayList al = new ArrayList(arr.length);
当我运行第一个声明(没有明确提供的大小)时,我看到的timediff为:
~96 milliseconds.
当我运行第二个声明(显式提供大小)时,timediff增加到:
~105 milliseconds
题:
为什么会这样呢?第二种情况(提供的尺寸)是否应该更快?
根据我的理解,在第一种情况下,当我们在ArrayList创建中省略size参数时,将在后台初始化长度为10的数组。并且当超出此容量时,将分配一个具有更大容量(不确定多少)的新数组,并且将复制以前的元素。
对于2236个元素且未指定初始大小,此"上限已超过-分配新的-复制完-追加更多直到上限"周期应重复多次,从而减慢其速度。
因此,我期望提供的size声明会更快-由于分配只会发生一次,因此不会出现容量超出/创建新阵列和复制的情况。
还是这基本上是因为2236追加到ArrayList上,即使在所有cap-exceeds-copy-over循环之后,仍然比创建大小为5,000,000的ArrayList还要快吗?
仅以System.currentTimeMillis()如此低的精度运行一次该脚本,并获得如此小的差异,将导致毫无意义的结果。执行1000次,然后使用System.nanoTime()开始测量接下来的1000次执行,然后取平均结果。
而不是创建具有完整数组大小的ArrayList,请尝试使用(int) Math.sqrt(arr.size()),它大约是理想平方的数量。如果要确定,请添加一些。
我会考虑在这里使用LinkedList。
@SidCool不好的主意。当我们谈论性能时,LinkedList绝不是一个选择。
@leventov该声明没有任何意义。何时从列表中删除元素呢? LinkedList还慢吗? ArrayList可以更快地访问第n个元素,但除此之外,它几乎没有什么区别,并且将删除或插入项目的链表好处。在没有上下文的情况下说一个比另一个要快是不正确的。
@Aaron:使用LinkedList时,即使追加和迭代也要慢得多(是2加上GC开销)。仅执行一项有益的操作:在Iterator位置移除。那几乎没有。尽管LinkedList有很好的用例,但比大多数人期望的要少得多。
@maaartinus好的插入和插入是两个不同的鱼缸。我只是说" LinkedList绝不是一个选择",这有点笼统的说法:D
@maaartinus尽管我一直在阅读,也许我的反对意见实际上是无效的! kjellkod.wordpress.com/2012/02/25/感谢您的指导。
@Aaron我什至认为将LinkedList添加到标准Java API中是一个错误。 LinkedList仅在非常罕见和特定的情况下才适用。在我6年的Java经验中,我从未使用过它。与TreeMap / Set,Deque和其他数据结构不同。在Github上,LinkedList的使用频率高于TreeMap,TreeSet,Deque和ArrayDeque的总和。真令人失望。
@leventov Ive比使用数组/列表更自然地将其与域模型匹配。 [Java]性能在我倾向于做的工作中很少出现问题,因为数据库访问/网络延迟通常是瓶颈。不过,感谢您的对话-现在让我更清晰了,因为只有领域模型认为它很自然才有意义。
当操作涉及大量插入时,@ leventov LinkedList很重要,因为它是一个双重链接列表。
仅需注意:ArrayList始终使用与2的幂次方相对应的大小进行初始化。当您指定初始大小时,它将实际初始化为下一个更高的2的幂。
您要创建一个500万个数组列表,所以显然它要慢一些。您只需要2236。这很浪费。
例如,如果将数组列表的大小更改为10k,则时差会缩小。
为简化起见,只需以不同的顺序多次尝试此测试-
public static void main(String[] args) {
long timea = System.currentTimeMillis();
// ArrayList al = new ArrayList();
ArrayList al = new ArrayList(5000000);
System.out.println(System.currentTimeMillis() - timea);
}
您会看到将一个数组列表初始化为500万个大约需要10毫秒(在我的Macbook上),而没有默认大小的数组列表几乎是瞬时的。这与您要测试的顺序相同。
这一点也不慢,如果您正确地进行基准测试,最终将导致2%以下的差异。
@ThomasJungblut速度较慢-运行测试看看。
运行卡尺基准测试并亲自查看结果:gist.github.com/thomasjungblut/7196135结果:99.6 ms与101 ms。 拷贝这么小的数字与直接分配更大的数组一样昂贵。
是否所有这些迭代都在同一VM中运行? 如果是这样的话,那就是另一种测试-一次运行一次以上代码,很明显,无论您测试哪个顺序(首先是初始化列表,还是仅是普通列表),大小为5000000的那个代码至少要花至少9倍的时间 十。 OP认为它不直观-原因是分配过多。
首先,您的测量方法存在缺陷。但是,在这些情况下,测量并不容易,因为对于如此大的阵列分配,每次新的分配可能会变慢。
至于您的问题:内存分配(甚至释放)是一项昂贵的操作。并非在您使用new时-如今vm已针对许多小型短期对象进行了优化-但大多数情况下,JVM必须在较低的系统级别上保留/分配(aka malloc())内存。此外,内存分配时间还取决于分配的内存量-您需要的内存越多,花费的时间就越长。
在您的情况下:理想平方的数量很容易计算。只需使用(Math.sqrt(arr.length) + 1)来确定初始ArrayList大小,然后立即获得正确的大小。
因为内存分配通常是一个缓慢的操作。我计算了两种情况下的分配数量和新元素。
在这种情况下
ArrayList al = new ArrayList();
您总共只能分配15个分配给8317个元素。在这种情况下
ArrayList al = new ArrayList(arr.length);
您只分配了5000000个元素。
看起来分配的相对大小很昂贵,而不是分配本身。
在已满的ArrayList上调用add()时,它会自动增长50%。因此它将很快足够大,并且不会有太多的内存分配。