Scala варьируется от производительности списков в больших коллекциях - PullRequest
4 голосов
/ 06 ноября 2011

Я провел набор тестов производительности для 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);
}

Требуется 141 миллисекунда с размером JVM 27m

Для сравнения, преобразование в списокрезко влияет на производительность:

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

Требуется 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));
    }

}

Требуется 3993 мс с размером JVM 283м

MyВопрос в том, почему первый пример так эффективен, а второй так сильно пострадал.Я пытался создать представления, но не смог воспроизвести преимущества производительности диапазона.

Все тесты, работающие на Mac OS X Snow Leopard, 64-разрядном сервере Java 6u26 Scala 2.9.1.final

РЕДАКТИРОВАТЬ:

для завершения, вот фактическая реализация, использующая LinkedList (который является более справедливым сравнением с точки зрения пространства, чем ArrayList, поскольку, как правильно указано, Scala's 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 мбс.Это намного больше соответствует производительности второго примера с 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 ]

12 голосов
/ 06 ноября 2011

О, так много всего происходит здесь !!!

Давайте начнем с Java int[]. Массивы в Java - единственная коллекция, которая не стерта. Представление int[] во время выполнения отличается от представления Object[] во время выполнения тем, что фактически использует int напрямую. Из-за этого в его использовании не участвует бокс.

С точки зрения памяти, у вас есть 40 000 000 последовательных байтов в памяти, которые считываются и записываются по 4 за раз, когда элемент читается или записывается.

Напротив, ArrayList<Integer> - как и почти любая другая универсальная коллекция - состоит из 40 000 000 или 80 000,00 последовательных байтов (в 32- и 64-битной JVM соответственно), ПЛЮС 80 000 000 байтов распределить все вокруг памяти группами по 8 байт. Каждое чтение и запись в элемент должны проходить через два пространства памяти, и само время, затрачиваемое на обработку всей этой памяти, является значительным, когда реальная задача, которую вы выполняете, настолько быстра.

Итак, вернемся к Scala, для второго примера, где вы манипулируете List. Теперь Scala List намного больше похож на Java 1016 *, чем грубо неверно названный ArrayList. Каждый элемент List состоит из объекта с именем Cons, который имеет 16 байтов, с указателем на элемент и указателем на другой список. Таким образом, List из 10.000.000 элементов состоит из 160.000.000 элементов, распределенных по всей памяти в группах по 16 байтов, плюс 80.000.000 байтов распределены по всей памяти в группах по 8 байтов. То, что было верно для ArrayList, тем более для List

Наконец, Range. Range - это последовательность целых чисел с нижней и верхней границами плюс шаг. A Range из 10.000.000 элементов составляет 40 байтов: три целых (не общих) для нижних и верхних границ и шага, плюс несколько предварительно вычисленных значений (last, numRangeElements) и два других целых числа, используемых для lazy val поток безопасности. Просто чтобы прояснить, это НЕ 40 раз 10.000.000: ИТОГО 40 байтов. Размер диапазона совершенно не имеет значения, потому что НЕ ХРАНИТ ИНДИВИДУАЛЬНЫХ ЭЛЕМЕНТОВ . Только нижняя граница, верхняя граница и шаг.

Теперь, поскольку Range является Seq[Int], для большинства случаев ему все равно придется пройти через бокс: int будет преобразован в Integer, а затем снова обратно в int, что печально расточительно.

Расчет размера минусов

Итак, вот предварительный расчет минусов. Прежде всего, прочитайте эту статью о некоторых общих рекомендациях относительно того, сколько памяти занимает объект. Важные моменты:

  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, то это указывает на еще один минус, который мы уже рассчитываем.

Я собираюсь пересмотреть оценки с фактическими данными, где это возможно.

4 голосов
/ 06 ноября 2011

В первом примере вы создаете связанный список с 10 элементами, вычисляя шаги диапазона.

Во втором примере вы создаете связанный список с 10 миллионами элементов и отфильтруйте его до нового связанного списка с 10 элементами.

В третьем примере вы создаете буфер с массивом с 10 миллионамиэлементов, которые вы просматриваете и печатаете, новый буфер на основе массива не создается.

Вывод:

Каждый фрагмент кода делает что-то своеВот почему производительность сильно варьируется.

1 голос
/ 06 ноября 2011

Трудно читать исходники Scala на моем iPad, но похоже, что конструктор Range на самом деле не создает список, а просто запоминает начало, приращение и конец. Он использует их для получения своих значений по запросу, так что итерация по диапазону намного ближе к простому циклу for, чем проверка элементов массива.

Как только вы говорите range.toList, вы заставляете Scala создавать связанный список «значений» в диапазоне (выделяя память как для значений, так и для ссылок), а затем вы перебираете это. Будучи связанным списком, производительность этого будет хуже, чем ваш пример Java ArrayList.

1 голос
/ 06 ноября 2011

Это обоснованное предположение ...

Я думаю, это потому, что в быстрой версии компилятор 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 предполагает, что это не так.)

...