-
Notifications
You must be signed in to change notification settings - Fork 69
SI-10167: Implement mutable.ArrayDeque #49
Changes from 37 commits
fcaa01a
834d9d7
51e4f4a
e6838e6
57dbc41
faba4cd
4f94cdd
aa20b6c
fa6c7e0
436a665
56ca57b
65133dc
fab70a1
ebbbb2a
a17f38c
5f137d5
e418495
b955281
c82c14e
aca8957
804bf43
595d6c5
7892282
0cde632
f7ed01c
c09de3a
1ecaf48
0f203ad
27a8e6d
49bb07a
c9063c6
64bc294
185d61c
68c9933
3640fa6
076d6f5
42ff0e3
bcab165
b0b6d78
38147a5
87ea88b
33c7e67
f0e8ea8
891e519
35ae179
36f27c4
d46dd19
6c48477
96d996d
1dfc995
2c93ade
2bb2c75
57b2fb6
1647625
360e4bf
959410d
b4cdc9b
de9c2ec
0c47056
efd6a47
ed1ace7
7addd8b
4b54e93
755261e
cb35330
968aa5f
f832409
7b2b9e1
8c4a20c
92f2252
d3d7155
b8c32a6
0519263
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,369 @@ | ||
| package strawman | ||
| package collection.mutable | ||
|
|
||
| import scala._ | ||
|
||
| import scala.collection.{GenSeq, generic, mutable} | ||
| import scala.reflect.ClassTag | ||
| import scala.Predef._ | ||
|
|
||
| import java.lang.Integer | ||
|
|
||
| /** An implementation of a double-ended queue that internally uses a resizable circular buffer | ||
| * Append, prepend, removeFirst, removeLast and random-access (indexed-lookup and indexed-replacement) | ||
| * take amortized constant time. In general, removals and insertions at i-th index are O(min(i, n-i)) | ||
| * and thus insertions and removals from end/beginning are fast. | ||
| * | ||
| * @author Pathikrit Bhowmick | ||
| * @version 2.13 | ||
| * @since 2.13 | ||
| * | ||
| * @tparam A the type of this ArrayDeque's elements. | ||
| * | ||
| * @define Coll `mutable.ArrayDeque` | ||
| * @define coll array deque | ||
| * @define thatinfo the class of the returned collection. In the standard library configuration, | ||
| * `That` is always `ArrayDeque[B]` because an implicit of type `CanBuildFrom[ArrayDeque, B, ArrayDeque[B]]` | ||
| * is defined in object `ArrayDeque`. | ||
| * @define bfinfo an implicit value of class `CanBuildFrom` which determines the | ||
| * result class `That` from the current representation type `Repr` | ||
| * and the new element type `B`. This is usually the `canBuildFrom` value | ||
| * defined in object `ArrayDeque`. | ||
| * @define orderDependent | ||
| * @define orderDependentFold | ||
| * @define mayNotTerminateInf | ||
| * @define willNotTerminateInf | ||
| */ | ||
| @SerialVersionUID(1L) | ||
| class ArrayDeque[A] private[ArrayDeque]( | ||
| private[ArrayDeque] var array: Array[AnyRef], | ||
| private[ArrayDeque] var start: Int, | ||
| private[ArrayDeque] var end: Int | ||
| ) extends mutable.AbstractBuffer[A] | ||
| with mutable.Buffer[A] | ||
| with generic.GenericTraversableTemplate[A, ArrayDeque] | ||
| with mutable.BufferLike[A, ArrayDeque[A]] | ||
| with mutable.IndexedSeq[A] | ||
| with mutable.IndexedSeqOptimized[A, ArrayDeque[A]] | ||
| with mutable.Builder[A, ArrayDeque[A]] | ||
| with Serializable { | ||
|
||
|
|
||
| private[this] var mask = 0 // modulus using bitmask since array.length is always power of 2 | ||
|
||
| resetInternal(array, start, end) | ||
|
|
||
| private[this] def resetInternal(array: Array[AnyRef], start: Int, end: Int) = { | ||
| this.array = array | ||
| this.mask = array.length - 1 | ||
| assert((array.length & mask) == 0, s"Array.length must be power of 2") | ||
| assert(0 <= start && start <= mask && 0 <= end && end <= mask) | ||
| this.start = start | ||
| this.end = end | ||
| } | ||
|
|
||
| def this(initialSize: Int = ArrayDeque.DefaultInitialSize) = this(ArrayDeque.alloc(initialSize), 0, 0) | ||
|
|
||
| override def apply(idx: Int) = { | ||
|
||
| ArrayDeque.checkIndex(idx, this) | ||
| getInternal(idx).asInstanceOf[A] | ||
| } | ||
|
|
||
| override def update(idx: Int, elem: A) = { | ||
| ArrayDeque.checkIndex(idx, this) | ||
| setInternal(idx, elem.asInstanceOf[AnyRef]) | ||
| } | ||
|
|
||
| override def +=(elem: A) = { | ||
| sizeHint(size) | ||
| appendAssumingCapacity(elem) | ||
| } | ||
|
|
||
| override def +=:(elem: A) = { | ||
| sizeHint(size) | ||
| prependAssumingCapacity(elem) | ||
| } | ||
|
|
||
| @inline private[ArrayDeque] def appendAssumingCapacity(elem: A): this.type = { | ||
| array(end) = elem.asInstanceOf[AnyRef] | ||
| end = end_-(-1) | ||
| this | ||
| } | ||
|
|
||
| @inline private[ArrayDeque] def prependAssumingCapacity(elem: A): this.type = { | ||
| start = start_+(-1) | ||
| array(start) = elem.asInstanceOf[AnyRef] | ||
| this | ||
| } | ||
|
|
||
| override def ++=:(elems: TraversableOnce[A]) = { | ||
| // Note this is faster than super.++=: because this does not need to convert TraversableOnce to a Traversable | ||
| if (elems.nonEmpty) { | ||
| ArrayDeque.knownSize(elems) match { | ||
| case srcLength if srcLength >= 0 && 2*(srcLength + this.length) >= mask => | ||
| val finalLength = srcLength + this.length | ||
| val array2 = ArrayDeque.alloc(finalLength) | ||
| elems.copyToArray(array2.asInstanceOf[Array[A]]) | ||
| copySliceToArray(srcStart = 0, dest = array2, destStart = srcLength, maxItems = size) | ||
| resetInternal(array = array2, start = 0, end = finalLength) | ||
| case _ => | ||
| val copy = clone() | ||
| end = start | ||
| this ++= elems ++= copy | ||
|
||
| } | ||
| } | ||
| this | ||
| } | ||
|
|
||
| override def insertAll(idx: Int, elems: scala.collection.Traversable[A]): Unit = { | ||
| ArrayDeque.checkIndex(idx, this) | ||
| if (elems.nonEmpty) { | ||
| ArrayDeque.knownSize(elems) match { | ||
| case srcLength if srcLength >= 0 => | ||
| val finalLength = srcLength + this.length | ||
| // Either we resize right away or move prefix right or suffix left | ||
| if (2*finalLength >= mask) { | ||
| val array2 = ArrayDeque.alloc(finalLength) | ||
| copySliceToArray(srcStart = 0, dest = array2, destStart = 0, maxItems = idx) | ||
| elems.copyToArray(array2.asInstanceOf[Array[A]], idx) | ||
| copySliceToArray(srcStart = idx, dest = array2, destStart = idx + srcLength, maxItems = size) | ||
| resetInternal(array = array2, start = 0, end = finalLength) | ||
| } else { | ||
| val suffix = drop(idx) | ||
| end = start_+(idx) | ||
| elems.foreach(appendAssumingCapacity) | ||
| suffix.foreach(appendAssumingCapacity) | ||
| } | ||
| case _ => //expensive to compute size | ||
| val suffix = drop(idx) | ||
| end = start_+(idx) | ||
| this ++= elems ++= suffix | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override def remove(idx: Int, count: Int): Unit = { | ||
| if (count < 0) throw new IllegalArgumentException(s"removing negative number of elements: $count") | ||
| ArrayDeque.checkIndex(idx, this) | ||
| if (count == 0) return | ||
|
||
| val removals = (size - idx) min count | ||
| // If we are removing more than half the elements, its cheaper to start over | ||
| // Else, choose the shorter: either move the prefix (0 until (idx + removals) right OR the suffix (idx to size) left | ||
| if (2*removals >= size) { | ||
|
||
| val array2 = ArrayDeque.alloc(size - removals) | ||
| copySliceToArray(srcStart = 0, dest = array2, destStart = 0, maxItems = idx) | ||
| copySliceToArray(srcStart = idx + removals - 1, dest = array2, destStart = idx, maxItems = size) | ||
| resetInternal(array = array2, start = 0, end = size - removals) | ||
| } else if (size - idx <= idx + removals) { | ||
| var i = idx | ||
| while(i + removals < size) { | ||
| setInternal(i, getInternal(i + removals)) | ||
| setInternal(i + removals, null) | ||
| i += 1 | ||
| } | ||
| nullify(from = i) | ||
| end = end_-(removals) | ||
| } else { | ||
| var i = idx + removals - 1 | ||
| while(i - removals >= 0) { | ||
| setInternal(i, getInternal(i - removals)) | ||
| setInternal(i - removals, null) | ||
| i -= 1 | ||
| } | ||
| nullify(until = i + 1) | ||
| start = start_+(removals) | ||
| } | ||
| } | ||
|
|
||
| override def remove(idx: Int) = { | ||
| val elem = this(idx) | ||
| remove(idx, 1) | ||
| elem | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param resizeInternalRepr If this is set, resize the internal representation to reclaim space once in a while | ||
| * @return | ||
| */ | ||
| def removeFirst(resizeInternalRepr: Boolean = false): Option[A] = { | ||
|
||
| if (isEmpty) { | ||
| None | ||
| } else { | ||
| val elem = array(start) | ||
| array(start) = null | ||
| start = start_+(1) | ||
| if (resizeInternalRepr && 2*size < mask) resize(size) | ||
| Some(elem.asInstanceOf[A]) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param resizeInternalRepr If this is set, resize the internal representation to reclaim space once in a while | ||
| * @return | ||
| */ | ||
| def removeLast(resizeInternalRepr: Boolean = false): Option[A] = { | ||
|
||
| if (isEmpty) { | ||
| None | ||
| } else { | ||
| end = end_-(1) | ||
| val elem = array(end) | ||
| array(end) = null | ||
| if (resizeInternalRepr && 2*size < mask) resize(size) | ||
| Some(elem.asInstanceOf[A]) | ||
| } | ||
| } | ||
|
|
||
| override def reverse = foldLeft(new ArrayDeque[A](initialSize = size))(_.prependAssumingCapacity(_)) | ||
|
|
||
| override def sizeHint(hint: Int) = if (hint >= mask) resize(hint + 1) | ||
|
||
|
|
||
| override def length = end_-(start) | ||
|
|
||
| override def isEmpty = start == end | ||
|
|
||
| override def nonEmpty = start != end | ||
|
|
||
| override def clone() = { | ||
| // TODO Scala's array.clone should call java.util.Arrays.copyOf(array, array.length) | ||
| new ArrayDeque(java.util.Arrays.copyOf(array, array.length), start, end) | ||
| } | ||
| /** | ||
| * Note: This does not actually resize the internal representation. | ||
| * See clearAndShrink if you want to also resize internally | ||
| */ | ||
| override def clear() = { | ||
| nullify() | ||
| start = 0 | ||
| end = 0 | ||
| } | ||
|
||
|
|
||
| /** | ||
| * clears this buffer and shrinks to @param size | ||
| * | ||
| * @param size | ||
| * @return | ||
| */ | ||
| def clearAndShrink(size: Int = ArrayDeque.DefaultInitialSize): this.type = { | ||
| require(size >= 0, s"Non-negative size required") | ||
| resetInternal(array = ArrayDeque.alloc(size), start = 0, end = 0) | ||
| this | ||
| } | ||
|
|
||
| override def slice(from: Int, until: Int) = { | ||
| val left = if (from <= 0) 0 else if (from >= size) size else from | ||
| val right = if (until <= 0) 0 else if (until >= size) size else until | ||
|
||
| val len = right - left | ||
| if (len <= 0) { | ||
| ArrayDeque.empty[A] | ||
| } else if (len >= size) { | ||
| clone() | ||
| } else { | ||
| val array2 = copySliceToArray(srcStart = left, dest = ArrayDeque.alloc(len), destStart = 0, maxItems = len) | ||
| new ArrayDeque(array2, 0, len) | ||
| } | ||
| } | ||
|
|
||
| override def sliding(window: Int, step: Int) = { | ||
| require(window > 0 && step > 0, s"window=$window and step=$step, but both must be positive") | ||
| val lag = (window - step) max 0 | ||
| Iterator.range(start = 0, end = length - lag, step = step).map(i => slice(i, i + window)) | ||
| } | ||
|
|
||
| override def grouped(n: Int) = sliding(n, n) | ||
|
|
||
| override def copyToArray[B >: A](dest: Array[B], destStart: Int, len: Int) = | ||
|
||
| copySliceToArray(srcStart = 0, dest = dest, destStart = destStart, maxItems = len) | ||
|
|
||
| override def companion = ArrayDeque | ||
|
|
||
| override def result() = this | ||
|
|
||
| override def stringPrefix = "ArrayDeque" | ||
|
|
||
| override def toArray[B >: A: ClassTag] = | ||
| copySliceToArray(srcStart = 0, dest = new Array[B](size), destStart = 0, maxItems = size) | ||
|
|
||
| /** | ||
| * This is a more general version of copyToArray - this also accepts a srcStart unlike copyToArray | ||
| * This copies maxItems elements from this collections srcStart to dest's destStart | ||
| * If we reach the end of either collections before we could copy maxItems, we simply stop copying | ||
| * | ||
| * @param dest | ||
| * @param srcStart | ||
| * @param destStart | ||
| * @param maxItems | ||
| */ | ||
| def copySliceToArray(srcStart: Int, dest: Array[_], destStart: Int, maxItems: Int): dest.type = { | ||
| ArrayDeque.checkIndex(srcStart, this) | ||
| ArrayDeque.checkIndex(destStart, dest) | ||
| val toCopy = maxItems min (size - srcStart) min (dest.length - destStart) | ||
| if (toCopy > 0) { | ||
| val startIdx = start_+(srcStart) | ||
| val block1 = toCopy min (array.length - startIdx) | ||
| Array.copy(src = array, srcPos = startIdx, dest = dest, destPos = destStart, length = block1) | ||
|
||
| if (block1 < toCopy) { | ||
| Array.copy(src = array, srcPos = 0, dest = dest, destPos = block1, length = toCopy - block1) | ||
| } | ||
| } | ||
| dest | ||
| } | ||
|
|
||
| /** | ||
| * Trims the capacity of this ArrayDeque's instance to be the current size | ||
| */ | ||
| def trimToSize(): Unit = resize(size - 1) | ||
|
|
||
| /** | ||
| * Add idx to start modulo mask | ||
| */ | ||
| @inline private[this] def start_+(idx: Int) = (start + idx) & mask | ||
|
||
|
|
||
| /** | ||
| * Subtract idx from end modulo mask | ||
| */ | ||
| @inline private[this] def end_-(idx: Int) = (end - idx) & mask | ||
|
|
||
| @inline private[this] def getInternal(idx: Int) = array(start_+(idx)) | ||
|
|
||
| @inline private[this] def setInternal(idx: Int, elem: AnyRef) = array(start_+(idx)) = elem | ||
|
|
||
| private[this] def nullify(from: Int = 0, until: Int = size) = { | ||
| var i = from | ||
| while(i < until) { | ||
| setInternal(i, null) | ||
| i += 1 | ||
| } | ||
| } | ||
|
|
||
| private[this] def resize(len: Int) = { | ||
| val array2 = copySliceToArray(srcStart = 0, dest = ArrayDeque.alloc(len), destStart = 0, maxItems = size) | ||
| resetInternal(array = array2, start = 0, end = size) | ||
| } | ||
| } | ||
|
|
||
| object ArrayDeque extends generic.SeqFactory[ArrayDeque] { | ||
| implicit def canBuildFrom[A]: generic.CanBuildFrom[Coll, A, ArrayDeque[A]] = | ||
| ReusableCBF.asInstanceOf[GenericCanBuildFrom[A]] | ||
|
|
||
| override def newBuilder[A]: mutable.Builder[A, ArrayDeque[A]] = new ArrayDeque[A]() | ||
|
|
||
| final val DefaultInitialSize = 8 | ||
|
|
||
| private[ArrayDeque] def knownSize[A](coll: TraversableOnce[A]) = { | ||
| /*coll.sizeHintIfCheap*/ -1 //TODO: Remove this temporary util when we switch to strawman .sizeHintIfCheap is now .knownSize | ||
| } | ||
|
|
||
| /** | ||
| * Allocates an array whose size is next power of 2 > $len | ||
| * Largest possible len is 1<<30 - 1 | ||
| * | ||
| * @param len | ||
| * @return | ||
| */ | ||
| private[ArrayDeque] def alloc(len: Int) = { | ||
| val size = ((1 << 31) >>> Integer.numberOfLeadingZeros(len max DefaultInitialSize)) << 1 | ||
| new Array[AnyRef](size.ensuring(_ >= 0, s"ArrayDeque too big - cannot allocate ArrayDeque of length $len")) | ||
| } | ||
|
|
||
| @inline private[ArrayDeque] def checkIndex(idx: Int, seq: GenSeq[_]) = | ||
| if (!seq.isDefinedAt(idx)) throw new IndexOutOfBoundsException(idx.toString) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this implementation supposed to fit in the current library or in the rewritten one? I thought it was supposed to go to this repo to fit in the rewritten library, but then it should not use the existing standard collection library. Instead:
strawman/collectionbut in packagescala.collection.mutable?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need
.sizeHintIfCheapwhich is package visible toscala.collection.mutableonly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if you put a class in package
scala.collection.mutable, it belongs in directoryscala/collection/mutable.But more fundamentally, IIUC collections in this repo don't use
scala.collectionat all, since the point of this repo is to completely redesign that package for 2.13. @julienrf am I missing something?I don't know if this should be ported to the 2.13 API, but it seems that should be clarified before focusing on low-level details.
I don't know how much effort it'd be to port this to
collection.strawman, but it's probably non-trivial.Because of the binary compatibility constraints, this code cannot be added to 2.12.x (and this would have been the wrong repo anyway) as long as it changes the API (even adding methods/classes is forbidden, because of forward compatibility—clients built with 2.12.2 must run against 2.12.0). It can be shipped as an external library for 2.12.x users, though it's maybe better then to put it in another package and give up on the optimization (adding to an existing package from a new JAR is forbidden by OSGi).
Because the APIs you're using will change a lot for 2.13, this code can't be merged as-is in 2.13 either.
Not my decision of course—it's just my understanding of the existing architectural rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This repo, as far as I understand, is a sandbox/playground type repo to prototype ideas before it lands in Scala. I just need access to
.sizeHintIfCheap- which would presumably exist in future Traversable too right?Making it use
collection.strawmanis also quite trivial. Just revert this commit: b955281It does not really use any other Scala collection besides Arrays underneath...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, indeed we should put new collections into
strawman.collection. That will make it easier to have both the old and new collections at the same time, so that we can compare them.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DarkDimius : So, this rewrite will happen for 2.13 or a further future version of Scala? I would like to get this into Scala sooner than later (if we have to wait for a full rewrite of collections). Should I send this pull request against Scala then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan is to target 2.13.
As @DarkDimius said, for now if you need something that’s related to the current collections you should just copy-paste it in the strawman.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pathikrit, yes. This is repo is targeting 2.13.
In order to release sooner you should target https://github.com/scala/scala, but I don't think your contribution will be included in 2.12 lifecycle due to binary compatibility considerations. If your collection is included in, lets say, 2.12.3, it's clearly incompatible with 2.12.0, as it didn't include this collection.
Based on this, I think that the earliest you collection can be included is next binary incompatible release, which is 2.13. And this means that you should target this repo and base your contributions on this strawman.
Another alternative would be for you to release it as an artifact which is a separate artifact on maven.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am fine with 2.13. Happy to make whatever changes necessary for that. I just don't want to wait for 2.14 or Dotty or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DarkDimius : Another alternative would be to include this as a private collection in scala 2.12.3 and change the underlying implementation of
mutable.{Queue, Stack, Buffer}to simply proxy to this - since this is strictly better than all of those (I have some hacky benchmarks but not a full suite to post yet).