scala范围与列表在大型集合上的性能

时间:2011-11-06 14:46:36

标签: java performance scala collections range

我为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毫升

非常感谢你的答案。

4 个答案:

答案 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,而不是名字错误的ArrayListList的每个元素都由一个名为Cons的对象组成,该对象有16个字节,带有指向该元素的指针和指向另一个列表的指针。因此,10.000.000个元素的List由160.000.000个元素组成,这些元素以16个字节为一组在内存中传播,加上80.000.000个字节以8个字节为一组遍布内存。那么对ArrayList

来说,List的真实情况更是如此

最后,RangeRange是一个整数序列,具有下边界和上边界,加上一个步长。 Range 10.000.000个元素是40个字节:三个整数(不是通用的)用于下限和上限以及步骤,加上一些预先计算的值(lastnumRangeElements)和另外两个用于lazy val线程安全的int。只是为了说清楚,那是 40倍10.000.000:那个40字节总计。范围的大小完全无关紧要,因为 IT DOESN&T存储个人元素。只是下限,上限和步。

现在,因为RangeSeq[Int],所以在大多数情况下仍然必须通过装箱:int将转换为Integer然后返回再次进入int,这是非常浪费的。

缺点尺寸计算

所以,这是对Cons的初步计算。首先,阅读this article关于对象占用多少内存的一般指导原则。重点是:

  1. Java使用8个字节作为普通对象,12个作为对象数组,用于&#34;内务管理&#34;信息(这个对象的类别等等)。
  2. 对象以8个字节的块分配。如果你的物体小于那个物体,它将被填充以补充它。
  3. 我实际上认为它是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示例更糟糕。