Skip to content

Commit

Permalink
Improve performance and behavior of ListMap and ListSet
Browse files Browse the repository at this point in the history
Makes the immutable `ListMap` and `ListSet` collections more alike one another, both in their semantics and in their performance.

In terms of semantics, makes the `ListSet` iterator return the elements in their insertion order, as `ListMap` already does. While, as mentioned in SI-8985, `ListMap` and `ListSet` doesn't seem to make any guarantees in terms of iteration order, I believe users expect `ListSet` and `ListMap` to behave in the same way, particularly when they are implemented in the exact same way.

In terms of performance, `ListSet` has a custom builder that avoids creation in O(N^2) time. However, this significantly reduces its performance in the creation of small sets, as its requires the instantiation and usage of an auxilliary HashSet. As `ListMap` and `ListSet` are only suitable for small sizes do to their performance characteristics, the builder is removed, the default `SetBuilder` being used instead.
  • Loading branch information
ruippeixotog committed May 9, 2016
1 parent 4c4c5e6 commit 11def59
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 51 deletions.
20 changes: 9 additions & 11 deletions src/library/scala/collection/immutable/ListMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ extends AbstractMap[A, B]
* @param xs the traversable object.
*/
override def ++[B1 >: B](xs: GenTraversableOnce[(A, B1)]): ListMap[A, B1] =
((repr: ListMap[A, B1]) /: xs.seq) (_ + _)
if (xs.isEmpty) this
else ((repr: ListMap[A, B1]) /: xs) (_ + _)

/** This creates a new mapping without the given `key`.
* If the map does not contain a mapping for the given key, the
Expand All @@ -123,15 +124,17 @@ extends AbstractMap[A, B]
*/
def - (key: A): ListMap[A, B] = this

private[ListMap] def unchecked_+[B1 >: B](kv: (A, B1)): ListMap[A, B1] = new Node(kv._1, kv._2)

/** Returns an iterator over key-value pairs.
*/
def iterator: Iterator[(A,B)] =
new AbstractIterator[(A,B)] {
var self: ListMap[A,B] = ListMap.this
def hasNext = !self.isEmpty
def next(): (A,B) =
if (!hasNext) throw new NoSuchElementException("next on empty iterator")
else { val res = (self.key, self.value); self = self.next; res }
if (hasNext) { val res = (self.key, self.value); self = self.next; res }
else Iterator.empty.next()
}.toList.reverseIterator

protected def key: A = throw new NoSuchElementException("empty map")
Expand Down Expand Up @@ -210,14 +213,9 @@ extends AbstractMap[A, B]
override def - (k: A): ListMap[A, B1] = remove0(k, this, Nil)

@tailrec private def remove0(k: A, cur: ListMap[A, B1], acc: List[ListMap[A, B1]]): ListMap[A, B1] =
if (cur.isEmpty)
acc.last
else if (k == cur.key)
(cur.next /: acc) {
case (t, h) => val tt = t; new tt.Node(h.key, h.value) // SI-7459
}
else
remove0(k, cur.next, cur::acc)
if (cur.isEmpty) acc.last
else if (k == cur.key) (cur.next /: acc) { case (t, h) => new t.Node(h.key, h.value) }
else remove0(k, cur.next, cur::acc)

override protected def next: ListMap[A, B1] = ListMap.this

Expand Down
45 changes: 9 additions & 36 deletions src/library/scala/collection/immutable/ListSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ package immutable

import generic._
import scala.annotation.tailrec
import mutable.{Builder, ReusableBuilder}

/** $factoryInfo
* @define Coll immutable.ListSet
Expand All @@ -23,33 +22,8 @@ object ListSet extends ImmutableSetFactory[ListSet] {
/** setCanBuildFromInfo */
implicit def canBuildFrom[A]: CanBuildFrom[Coll, A, ListSet[A]] = setCanBuildFrom[A]

override def newBuilder[A]: Builder[A, ListSet[A]] = new ListSetBuilder[A]

private object EmptyListSet extends ListSet[Any] { }
private[collection] def emptyInstance: ListSet[Any] = EmptyListSet

/** A custom builder because forgetfully adding elements one at
* a time to a list backed set puts the "squared" in N^2. There is a
* temporary space cost, but it's improbable a list backed set could
* become large enough for this to matter given its pricy element lookup.
*
* This builder is reusable.
*/
class ListSetBuilder[Elem](initial: ListSet[Elem]) extends ReusableBuilder[Elem, ListSet[Elem]] {
def this() = this(empty[Elem])
protected val elems = (new mutable.ListBuffer[Elem] ++= initial).reverse
protected val seen = new mutable.HashSet[Elem] ++= initial

def +=(x: Elem): this.type = {
if (!seen(x)) {
elems += x
seen += x
}
this
}
def clear() = { elems.clear() ; seen.clear() }
def result() = elems.foldLeft(empty[Elem])(_ unchecked_+ _)
}
}

/** This class implements immutable sets using a list-based data
Expand Down Expand Up @@ -104,7 +78,7 @@ sealed class ListSet[A] extends AbstractSet[A]
*/
override def ++(xs: GenTraversableOnce[A]): ListSet[A] =
if (xs.isEmpty) this
else (new ListSet.ListSetBuilder(this) ++= xs.seq).result()
else (repr /: xs) (_ + _)

private[ListSet] def unchecked_+(e: A): ListSet[A] = new Node(e)
private[ListSet] def unchecked_outer: ListSet[A] =
Expand All @@ -119,13 +93,9 @@ sealed class ListSet[A] extends AbstractSet[A]
var that: ListSet[A] = self
def hasNext = that.nonEmpty
def next: A =
if (hasNext) {
val res = that.head
that = that.tail
res
}
if (hasNext) { val res = that.head; that = that.tail; res }
else Iterator.empty.next()
}
}.toList.reverseIterator

/**
* @throws java.util.NoSuchElementException
Expand Down Expand Up @@ -174,9 +144,12 @@ sealed class ListSet[A] extends AbstractSet[A]

/** `-` can be used to remove a single element from a set.
*/
override def -(e: A): ListSet[A] = if (e == head) self else {
val tail = self - e; new tail.Node(head)
}
override def -(e: A): ListSet[A] = removeInternal(e, this, Nil)

@tailrec private def removeInternal(k: A, cur: ListSet[A], acc: List[ListSet[A]]): ListSet[A] =
if (cur.isEmpty) acc.last
else if (k == cur.head) (cur.tail /: acc) { case (t, h) => new t.Node(h.head) }
else removeInternal(k, cur.tail, cur :: acc)

override def tail: ListSet[A] = self
}
Expand Down
4 changes: 2 additions & 2 deletions test/files/jvm/serialization-new.check
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ x = Map(buffers -> 20, layers -> 2, title -> 3)
y = Map(buffers -> 20, layers -> 2, title -> 3)
x equals y: true, y equals x: true

x = ListSet(5, 3)
y = ListSet(5, 3)
x = ListSet(3, 5)
y = ListSet(3, 5)
x equals y: true, y equals x: true

x = Queue(a, b, c)
Expand Down
4 changes: 2 additions & 2 deletions test/files/jvm/serialization.check
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ x = Map(buffers -> 20, layers -> 2, title -> 3)
y = Map(buffers -> 20, layers -> 2, title -> 3)
x equals y: true, y equals x: true

x = ListSet(5, 3)
y = ListSet(5, 3)
x = ListSet(3, 5)
y = ListSet(3, 5)
x equals y: true, y equals x: true

x = Queue(a, b, c)
Expand Down
16 changes: 16 additions & 0 deletions test/junit/scala/collection/immutable/ListMapTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package scala.collection.immutable

import org.junit.Assert._
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(classOf[JUnit4])
class ListMapTest {

@Test
def hasCorrectBuilder(): Unit = {
val m = ListMap[String, String]("a" -> "1", "b" -> "2", "c" -> "3", "b" -> "2.2", "d" -> "4")
assertEquals(List("a" -> "1", "c" -> "3", "b" -> "2.2", "d" -> "4"), m.toList)
}
}
16 changes: 16 additions & 0 deletions test/junit/scala/collection/immutable/ListSetTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package scala.collection.immutable

import org.junit.Assert._
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(classOf[JUnit4])
class ListSetTest {

@Test
def hasTailRecursiveDelete(): Unit = {
val s = ListSet[Int](1 to 50000: _*)
try s - 25000 catch { case e: StackOverflowError => fail("A stack overflow occurred") }
}
}

0 comments on commit 11def59

Please sign in to comment.