【问题标题】:scala ranges versus lists performance on large collectionsscala 范围与列表在大型集合上的性能
【发布时间】:2011-12-23 02:09:53
【问题描述】:

我针对 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)
}

460-455 m 需要 8514-10896 ms

相比之下,这个 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));
    }
}

JVM 大小为 59m 需要 116ms

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 毫秒

我的问题是,为什么第一个示例如此高效,而第二个示例却受到如此严重的影响。我尝试创建视图,但未能成功再现该范围的性能优势。

在 Mac OS X Snow Leopard 上运行的所有测试, Java 6u26 64 位服务器 斯卡拉 2.9.1.final

编辑:

为了完成,这里是使用 LinkedList 的实际实现(这在空间方面比 ArrayList 更公平,因为正如正确指出的那样,scala 的列表是链接的)

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);
                    }
            }
    }

}

480-460 mbs 需要 3621-6749 毫秒。这更符合第二个 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 mb

非常感谢您的回答。

【问题讨论】:

  • 请注意,Java 的 LinkedList 是双向链接的,因此每个单元格除了 Scala 的尾部引用外,还有一个反向引用。
  • 无需借助 Java 代码来使用原语。 Array[Int] 将在幕后使用原语。等效代码应该和 Java 版本的速度一样。
  • 我想知道你为什么用米来衡量 JVM 的大小...... :-)

标签: java performance scala collections range


【解决方案1】:

哦,这里发生了很多事情!!!

让我们从 Java int[] 开始。 Java 中的数组是唯一没有类型擦除的集合。 int[] 的运行时表示不同于Object[] 的运行时表示,因为它实际上直接使用int。因此,使用它时不涉及拳击。

在内存方面,内存中有 40.000.000 个连续字节,每当读取或写入一个元素时,每次读取和写入 4 个字节。

相比之下,ArrayList&lt;Integer&gt;——以及几乎任何其他通用集合——由 40.000.000 或 80.000.00 个连续字节(分别在 32 位和 64 位 JVM 上),加上 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 是具有上下边界的整数序列,外加一个步长。 10.000.000 个元素的Range 为 40 个字节:三个整数(非通用)用于上下界和步长,加上一些预先计算的值(lastnumRangeElements)和用于@ 的其他两个整数987654341@ 线程安全。为了清楚起见,NOT 40 乘以 10.000.000:总共 40 个字节。范围的大小完全无关紧要,因为它不存储单个元素。只有下限、上限和步长。

现在,因为RangeSeq[Int],所以它在大多数情况下仍然需要经过拳击:int 将被转换为Integer,然后再次转换为int,这可悲的是浪费。

缺点大小计算

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

  1. Java 为普通对象使用 8 个字节,为对象数组使用 12 个字节,用于“管理”信息(该对象的类是什么等)。
  2. 对象以 8 个字节的块分配。如果您的对象小于此值,则会对其进行填充以补充它。

其实我以为是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,它指向另一个缺点,我们已经在计算。

我将修改估计值,并尽可能使用实际数据。

【讨论】:

  • 为什么一个 cons 单元格这么大?除了 car 和 cdr 还存储什么?
  • @DuncanMcGregor 实际上,它并没有那么大。我认为这是一个大于 16 字节的little,并且 Java 分配为 16 字节块。事实证明,它分配了 8 个字节的块,而 Cons 正好是 16 个字节长。我已将此信息附加到答案中。
  • @Daniel C. Sobral 在所有答案中,这是最彻底的一个。本质上,我一直在寻找的是,第一个实现使用的空间比存储每个项目所需的空间要少得多。实际上,因为 Range 实际上并不存储它们,只是跟踪它们。值得注意的是,即使与简单的 Java“ArrayList”相比,在第二个示例中创建显式列表的差异也会产生重大影响。
  • @fracca 惩罚在于filter,它调用了Function1。由于间接,这会导致装箱问题和更糟糕的 JIT 优化。
【解决方案2】:

在第一个示例中,您通过计算范围的步长来创建一个包含 10 个元素的链接列表

在第二个示例中,您创建了一个包含 1000 万个元素的 链表,并将其过滤成一个包含 10 个元素的新 链表

在第三个示例中,您创建了一个 array-backed buffer,其中包含您遍历和打印的 1000 万个元素,没有创建新的 array-backed buffer。 p>

结论:

每段代码都有不同的作用,这就是性能差异很大的原因。

【讨论】:

    【解决方案3】:

    这是一个有根据的猜测......

    我认为这是因为在快速版本中,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 大小表明它不是。)

    【讨论】:

    • 这个假设似乎是正确的,否则它至少需要与原始版本一样多的内存。我在两个版本的 scala 代码上运行了 scalap,但在输出中看不到任何差异。谢谢!
    • 不太正确。不是编译器在这里做特殊的事情,而只是 Range 的实现,它计算元素而不是分配它们。
    【解决方案4】:

    在我的 iPad 上很难阅读 Scala 源代码,但看起来 Range 的构造函数实际上并没有生成列表,只是记住了开始、递增和结束。它使用这些来根据请求生成其值,因此在一个范围内迭代比检查数组的元素更接近于简单的 for 循环。

    只要你说range.toList,你就是在强制 Scala 生成一个范围内“值”的链表(为值和链接分配内存),然后你正在迭代它。作为一个链表,它的性能会比你的 Java ArrayList 示例差。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-06-18
      • 2016-06-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-22
      • 2019-09-17
      • 2013-04-20
      • 1970-01-01
      相关资源
      最近更新 更多