2011-11-06 96 views
4

我爲10,000,000個元素運行了一組性能基準,並且我發現每個實現的結果都有很大不同。scala範圍與大型集合上的性能對比列表

任何人都可以解釋爲什麼創建一個Range.ByOne,導致性能比簡單的基元數組更好,但是將相同的範圍轉換爲列表會導致比最壞情況更糟的性能?

創建10,000,000個元素,並打印出模數爲1,000,000的元素。 JVM大小總是設置爲相同的最小值和最大值:??-Xms米-Xmx米

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

它需要141毫秒與JVM尺寸2700萬

相比之下,轉換成列表影響性能大幅提升:

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毫秒460-455米

與此相反,該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)); 
    } 
} 

這需要116ms與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)); 
    } 

}

這需要3993毫秒JVM大小的283米

我的問題是,爲什麼第一個例子如此高效,而第二個則受到如此嚴重的影響。我嘗試創建視圖,但未能成功重現該範圍的性能優勢。

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

} 

注意到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 MB

非常感謝您的答案。

+0

請注意,Java的'LinkedList'是雙重鏈接的,所以除了Scala的尾部引用之外,每個單元都有一個反向引用。 –

+0

沒有必要訴諸Java代碼來使用原語。 'Array [Int]'將在封面下使用原語。等效代碼應該與Java版本的速度相同。 –

+0

我想知道你爲什麼要測量JVM的大小,以米爲單位::-) – PhiLho

回答

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個字節爲一組。每一次讀取元素的寫入都必須經過兩個內存空間,而當你正在執行的實際任務如此之快時,處理所有內存所花費的時間是非常重要的。

所以,回到斯卡拉,對於第二個例子,你操縱List。現在,斯卡拉的List更像是Java的LinkedList比嚴重錯誤的ArrayListList的每個元素都由一個名爲Cons的對象組成,該對象具有16個字節,其中包含指向元素的指針和指向另一個列表的指針。因此,10.000.000個元素的List由160.000.000個元素組成,每個元素在16個字節的組中分佈在所有內存中,外加80.000.000個字節以8個字節組的形式分佈在所有內存中。那麼對於ArrayList來說什麼對於List

更是如此,最後是Range。 A Range是一個具有較低和較高邊界的整數序列,再加上一個步驟。包含10.000.000個元素的Range爲40個字節:對於下限和上限以及步驟,加上一些預先計算的值(last,numRangeElements)以及用於lazy val線程安全的兩個其他整數(三個整數)。只是爲了表明,這是不是 40倍10.000.000:這是40字節總數。範圍的大小是完全不相關的,因爲它不存儲個人元素。只是下限,上限和步驟。現在

,因爲RangeSeq[Int],它仍然要經過拳擊在大多數應用中:一個int將被再次轉換成Integer,然後返回到int,這是可悲的浪費。

缺點尺寸計算

所以,這裏的缺點的試算。首先,請閱讀this article,瞭解有關物體佔用多少內存的一些通用指南。重點是:

  1. Java使用8個字節的普通對象和12個對象數組,對於「內務」信息(這個對象的類是什麼等)。
  2. 對象以8字節塊分配。如果你的對象比那個小,它將被填充來補充它。

其實我認爲這是16個字節,而不是8無論如何,缺點也比我想象的要小。其字段包括:

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,它指向另一個缺點,我們已經在計算。

我打算修改估算,並儘可能使用實際數據。

+0

爲什麼一個cons cell如此之大?它除了汽車和司機之外還有什麼其他商店? –

+0

@DuncanMcGregor其實它並沒有那麼大。我認爲這是一個大於16字節的_little_,並且以16字節的塊分配Java。事實證明,它分配了8個字節的塊,而這個Cons的長度恰好是16個字節。我已將這些信息添加到答案中。 –

+0

@Daniel C. Sobral在所有答案中,這是最徹底的答案。從本質上講,我一直在尋找的是,實際上,第一個實現使用的空間比存儲每個項目所需的空間少得多。實際上,因爲Range並不實際存儲它們,所以只需跟蹤它們。不過有趣的是,與第二個例子相比,在簡單的Java「ArrayList」中創建一個明確的列表的差別甚至相當大。 – fracca

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的大小表明,事實並非如此。)

+0

這個假設似乎是正確的,否則至少需要與原始版本一樣多的內存。我在兩個版本的scala代碼上運行scalap,但是看不出輸出有什麼不同。謝謝! – fracca

+2

不是很正確。不是編譯器在這裏做特殊的東西,而只是執行Range來計算元素而不是分配它們。 – soc

1

很難閱讀斯卡拉源上我的iPad,但它看起來像Range的構造實際上並沒有列出清單,只記得開始,增加和結束。它使用這些來根據請求產生它的值,所以迭代範圍比檢查數組元素更接近簡單的for循環。

只要你說range.toList你迫使Scala在範圍內產生一個'values'的鏈表(爲值和鏈接分配內存),然後你在迭代它。作爲一個鏈表,這個表現會比你的Java ArrayList示例更糟糕。

4

在第一個示例中,通過計算範圍的步驟創建一個帶有10個元素的鏈接列表

在第二個例子中,你創建一個鏈表 10百萬元,並有10個元素篩選下來到一個新的鏈表

在第三示例中在創建陣列支持緩衝器用10百萬你遍歷和打印元件,則不創建新的陣列支持緩衝器

結論:

每一段代碼做不同的東西,這就是爲什麼性能差別很大。