我为10,000,000个元素运行了一组性能基准测试,并且我发现每个实现的结果差别很大。
任何人都可以解释为什么创建一个Range.ByOne会导致性能优于简单的基元数组,但将相同范围转换为列表会导致性能甚至比最坏的情况更差吗?
创建10,000,000个元素,并打印出1,000,000个模数的元素。 JVM大小始终设置为相同的最小值和最大值:-Xms?m -Xmx?m
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit._
object LightAndFastRange extends App {
def chrono[A](f: => A, timeUnit: TimeUnit = MILLISECONDS): (A,Long) = {
val start = System.nanoTime()
val result: A = f
val end = System.nanoTime()
(result, timeUnit.convert(end-start, NANOSECONDS))
}
def millions(): List[Int] = (0 to 10000000).filter(_ % 1000000 == 0).toList
val results = chrono(millions())
results._1.foreach(x => println ("x: " + x))
println("Time: " + results._2);
}
JVM大小为27m
需要141毫秒相比之下,转换为List会显着影响性能:
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit._
object LargeLinkedList extends App {
def chrono[A](f: => A, timeUnit: TimeUnit = MILLISECONDS): (A,Long) = {
val start = System.nanoTime()
val result: A = f
val end = System.nanoTime()
(result, timeUnit.convert(end-start, NANOSECONDS))
}
val results = chrono((0 to 10000000).toList.filter(_ % 1000000 == 0))
results._1.foreach(x => println ("x: " + x))
println("Time: " + results._2)
}
需要8514-10896 ms,460-455 m
相比之下,这个Java实现使用了一个基元数组
import static java.util.concurrent.TimeUnit.*;
public class LargePrimitiveArray {
public static void main(String[] args){
long start = System.nanoTime();
int[] elements = new int[10000000];
for(int i = 0; i < 10000000; i++){
elements[i] = i;
}
for(int i = 0; i < 10000000; i++){
if(elements[i] % 1000000 == 0) {
System.out.println("x: " + elements[i]);
}
}
long end = System.nanoTime();
System.out.println("Time: " + MILLISECONDS.convert(end-start, NANOSECONDS));
}
}
需要116毫秒,JVM大小为59米
Java整数列表
import java.util.List;
import java.util.ArrayList;
import static java.util.concurrent.TimeUnit.*;
public class LargeArrayList {
public static void main(String[] args){
long start = System.nanoTime();
List<Integer> elements = new ArrayList<Integer>();
for(int i = 0; i < 10000000; i++){
elements.add(i);
}
for(Integer x: elements){
if(x % 1000000 == 0) {
System.out.println("x: " + x);
}
}
long end = System.nanoTime();
System.out.println("Time: " + MILLISECONDS.convert(end-start, NANOSECONDS));
}
}
JVM大小为283m
需要3993 ms我的问题是,为什么第一个例子如此高效,而第二个例子受到的影响非常严重。我尝试创建视图,但没有成功再现该范围的性能优势。
在Mac OS X Snow Leopard上运行的所有测试, Java 6u26 64位服务器 Scala 2.9.1.final
编辑:
为了完成,这里是使用LinkedList的实际实现(在空间方面比ArrayList更公平的比较,因为正确地指出,scala的List是链接的)
import java.util.List;
import java.util.LinkedList;
import static java.util.concurrent.TimeUnit.*;
public class LargeLinkedList {
public static void main(String[] args){
LargeLinkedList test = new LargeLinkedList();
long start = System.nanoTime();
List<Integer> elements = test.createElements();
test.findElementsToPrint(elements);
long end = System.nanoTime();
System.out.println("Time: " + MILLISECONDS.convert(end-start, NANOSECONDS));
}
private List<Integer> createElements(){
List<Integer> elements = new LinkedList<Integer>();
for(int i = 0; i < 10000000; i++){
elements.add(i);
}
return elements;
}
private void findElementsToPrint(List<Integer> elements){
for(Integer x: elements){
if(x % 1000000 == 0) {
System.out.println("x: " + x);
}
}
}
}
需要3621-6749毫秒,480-460 mbs。这更符合第二个scala示例的性能。
最后,一个LargeArrayBuffer
import collection.mutable.ArrayBuffer
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit._
object LargeArrayBuffer extends App {
def chrono[A](f: => A, timeUnit: TimeUnit = MILLISECONDS): (A,Long) = {
val start = System.nanoTime()
val result: A = f
val end = System.nanoTime()
(result, timeUnit.convert(end-start, NANOSECONDS))
}
def millions(): List[Int] = {
val size = 10000000
var items = new ArrayBuffer[Int](size)
(0 to size).foreach (items += _)
items.filter(_ % 1000000 == 0).toList
}
val results = chrono(millions())
results._1.foreach(x => println ("x: " + x))
println("Time: " + results._2);
}
大约需要2145毫秒和375毫升
非常感谢你的答案。
答案 0 :(得分:12)
哦,这里发生了很多事情!!!
让我们从Java int[]
开始。 Java中的数组是唯一不被类型擦除的集合。 int[]
的运行时表示与Object[]
的运行时表示不同,因为它实际上直接使用int
。因此,使用它没有拳击。
在内存方面,内存中有40.000.000个连续字节,每当读取或写入一个元素时,每次读取和写入4个字节。
相比之下,ArrayList<Integer>
- 以及几乎任何其他通用集合 - 由40.000.000或80.000.00个连续字节组成(分别在32位和64位JVM上),PLUS 80.000。 000字节以8字节为一组在内存中传播。每次读取元素的写入都必须通过两个存储空间,当你正在进行的实际任务如此之快时,处理所有内存所花费的时间非常重要。
所以,回到Scala,第二个例子是你操纵List
。现在,Scala的List
更像是Java LinkedList
,而不是名字错误的ArrayList
。 List
的每个元素都由一个名为Cons
的对象组成,该对象有16个字节,带有指向该元素的指针和指向另一个列表的指针。因此,10.000.000个元素的List
由160.000.000个元素组成,这些元素以16个字节为一组在内存中传播,加上80.000.000个字节以8个字节为一组遍布内存。那么对ArrayList
List
的真实情况更是如此
最后,Range
。 Range
是一个整数序列,具有下边界和上边界,加上一个步长。 Range
10.000.000个元素是40个字节:三个整数(不是通用的)用于下限和上限以及步骤,加上一些预先计算的值(last
,numRangeElements
)和另外两个用于lazy val
线程安全的int。只是为了说清楚,那是不 40倍10.000.000:那个40字节总计。范围的大小完全无关紧要,因为 IT DOESN&T存储个人元素。只是下限,上限和步。
现在,因为Range
是Seq[Int]
,所以在大多数情况下仍然必须通过装箱:int
将转换为Integer
然后返回再次进入int
,这是非常浪费的。
缺点尺寸计算
所以,这是对Cons的初步计算。首先,阅读this article关于对象占用多少内存的一般指导原则。重点是:
我实际上认为它是16个字节,而不是8.无论如何,Cons也比我想象的要小。它的领域是:
public static final long serialVersionUID; // static, doesn't count
private java.lang.Object scala$collection$immutable$$colon$colon$$hd;
private scala.collection.immutable.List tl;
引用至少 4个字节(在64位JVM上可能更多)。所以我们有:
8 bytes Java header
4 bytes hd
4 bytes tl
这使得它只有16个字节长。实际上非常好。在示例中,hd
将指向Integer
对象,我假设该对象长度为8个字节。至于tl
,它指向另一个我们已经在计算的缺点。
我将尽可能修改估算值,并提供实际数据。
答案 1 :(得分:4)
在第一个示例中,您通过计算范围的步数来创建包含10个元素的链接列表。
在第二个示例中,您创建了一个包含10百万个元素的链接列表,并将其过滤为包含10个元素的新链接列表。
在第三个示例中,您将创建一个阵列支持的缓冲区,其中包含您遍历和打印的数百万个元素,不会创建新的阵列支持的缓冲区。
<强>结论:强>
每一段代码都有不同之处,这就是性能差异很大的原因。
答案 2 :(得分:1)
这是一个有根据的猜测...
我认为这是因为在快速版本中,Scala编译器能够将密钥语句转换为类似的东西(在Java中):
List<Integer> millions = new ArrayList<Integer>();
for (int i = 0; i <= 10000000; i++) {
if (i % 1000000 == 0) {
millions.add(i);
}
}
如您所见,(0 to 10000000)
不会生成10,000,000个Integer
个对象的中间列表。
相比之下,在慢速版本中,Scala编译器无法进行优化,并且正在生成该列表。
(中间数据结构可能是int[]
,但观察到的JVM大小表明它不是。)
答案 3 :(得分:1)
我的iPad上很难读取Scala源代码,但看起来Range
的构造函数实际上并没有生成列表,只记住了开始,增量和结束。它使用这些来根据请求生成它的值,因此迭代一个范围比检查数组的元素更接近简单的for循环。
一旦你说range.toList
,你就强迫Scala生成范围内'值'的链接列表(为值和链接分配内存),然后你在迭代它。作为链表,其性能将比Java ArrayList示例更糟糕。