-
Notifications
You must be signed in to change notification settings - Fork 70
SI-10167: Implement mutable.ArrayDeque #49
Conversation
|
||
@inline private def checkIndex(idx: Int, seq: GenSeq[_] = this) = if(!seq.isDefinedAt(idx)) throw new IndexOutOfBoundsException(idx.toString) | ||
|
||
@inline private def box(i: Int) = if (i <= 0) 0 else if (i >= size) size else i |
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 is not a good choice of name for what this does. clip
is fine (or something longer), but "box" usually refers to wrapping of a value in another object.
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.
Seems like the intention here is to find a location at which to slice, so maybe call this slicePoint
.
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.
Also this could be written as i max 0 min size
. I don't have a strong opinion either way but if you're using max
and min
already you might as well use them consistently throughout the code.
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.
For inlined code, it's better to avoid value class invocations of numeric methods--the JVM has to work hard to get rid of all of the extra stuff left in the bytecode after you type i max 0 min size
. The if-statement is preferable.
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.
@Ichoran Fine with me. I saw the min
and max
methods used elsewhere here so I'm mainly advocating for some consistency. We can use if
statements or math.min
/math.max
directly.
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.
For inlined code, it's better to avoid value class invocations of numeric methods--the JVM has to work hard to get rid of all of the extra stuff left in the bytecode after you type
i max 0 min size
OT: That's probably true but it saddens me greatly Scalac lacks an optimizer and requires us to write assembly.
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.
@Ichoran: "boxing" a number into a range is standard parlance but in context of jvm, I agree it might be misleading. Changed it to fencePost
fen
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.
@gmethvin : this function is a critical section of the code.. so preferring if here. The places where I did use min/max are in non-critical paths (e.g. during amortized growing) or places that are O(n) anyway..
var i = len max defaultInitialSize | ||
//See: http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 | ||
i |= i >> 1; i |= i >> 2; i |= i >> 4; i |= i >> 8; i |= i >> 16 | ||
Array.ofDim[AnyRef](i + 1) |
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.
Why not just new Array[AnyRef](i+1)
and cut out all the indirection?
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.
done
private def ensureCapacity() = if (size == array.length - 1) resize(array.length) | ||
|
||
private def resize(len: Int) = { | ||
val array2 = ArrayDeque.alloc(len) |
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 it worth special-casing small additions to avoid the sequential bit-shifting and all these tests when you're just adding a single element at a time? At large sizes it doesn't matter but at small sizes the five shift/or/equals might be a non-negligible part of the total runtime.
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 think this is reasonably since the default initial size is 16 cells. If you look at Java's ArrayDeque - it uses the same idea..
set(array = array2, start = 0, end = size - removals) | ||
} else if (size - idx <= idx + removals) { | ||
(idx until size) foreach {i => | ||
val elem = if (i + removals < size) get(i + removals) else null |
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.
Having this conditional inside the loop seems like a bad idea.
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.
Good idea - will break into 2 loops
} | ||
|
||
override def ++=:(xs: TraversableOnce[A]) = | ||
xs.foldRight(this)((x, coll) => x +=: coll).asInstanceOf[this.type] |
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 looks correct but is hardly an efficient implementation. Can you mark this in some way for later improvement? (foldRight is an expensive operation for a lot of collections)
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.
Hmm, what would be an efficient one? I will add a TODO here for now...
*/ | ||
def trimToSize(): Unit = resize(size - 1) | ||
|
||
@inline private def checkIndex(idx: Int, seq: GenSeq[_] = this) = 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.
This seems like too much generality for the fastest cases, and is a bit confusing besides. (Since the version without this
seems like it ought to somehow be aware of this
. But it isn't.)
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.
okay, will remove the default param...
|
||
override def grouped(n: Int) = sliding(n, n) | ||
|
||
override def copyToArray[B >: A](dest: Array[B], destStart: Int, len: Int) = |
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.
Having this extra method that just overrides the second argument to be 0 while having a different name seems awkward. ("What is copyToArray? What is arrayCopy? Why aren't these the same, and which one is more general?")
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.
well copyToArray is something we inherit (see it has override on it). The arrayCopy is much more general than the one I am forced to overwrite since it also takes in a srcStart
unlike copyToArray
. Will document this.
|
||
override def newBuilder[A]: mutable.Builder[A, ArrayDeque[A]] = new ArrayDeque[A]() | ||
|
||
val defaultInitialSize = 8 |
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.
Since this is a constant it should be DefaultInitialSize
.
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.
Everything's a val (constant) in Scala?
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.
why not DEFAULT_INITIAL_SIZE
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.
why not final val DEFAULT_INITIAL_SIZE = 8
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.
@hepin1989 I think you are confusing Java with Scala. final here in Scala means it is not overridable and not that it is a const
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 - Actually, SLS section 4.1 says,
A constant value definition is of the form
final val x = e
where e
is a constant expression (§6.24). The final
modifier must be present and
no type annotation may be given. References to the constant value x
are themselves
treated as constant expressions; in the generated code they are replaced by the def-
inition’s right-hand side e
.
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.
And FWIW this does make a significant performance difference in tight code (e.g. constants used for simple random number generators).
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.
@Ichoran : I agree but we are talking about the naming of the variable with a capital letter or not here :)
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'm referring to constants as the term is used in the Scala style guide:
Constant names should be in upper camel case. Similar to Java’s static final members, if the member is final, immutable and it belongs to a package object or an object, it may be considered a constant
I don't have a really strong opinion on this, but I assumed that we would follow the official Scala style guide here. That seems to distinguish constants from simple values.
Also, I agree with @Ichoran the final
modifier should be added.
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.
val removals = (size - idx) min count | ||
// If we are removing more than half the elements, its cheaper to start over | ||
// Else, either move the prefix right or the suffix left - whichever is shorter | ||
if(2*removals >= size) { |
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.
if (
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.
fixed
} | ||
|
||
override def ++=:(xs: TraversableOnce[A]) = | ||
xs.foldRight(this)((x, coll) => x +=: coll).asInstanceOf[this.type] |
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.
Couldn't this use insertAll
?
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 is ++=:
(prependAll
) not ++=
(insertAll
)
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.
prependAll(xs)
is the same as insertAll(0, xs)
, no? I assumed that making sure the space is available beforehand and then copying the elements would be more efficient than adding them one by one like we do here.
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.
Doh, you are right! In fact, that's what it is in the super. Will remove.
|
||
def this(initialSize: Int = ArrayDeque.defaultInitialSize) = this(ArrayDeque.alloc(initialSize), 0, 0) | ||
|
||
override def apply(idx: Int) = { |
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.
It would be good to have an explicit type annotation on these methods.
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 usually don't put the types if I put override
since its redundant (you cant make a mistake here) but not a strong opinion
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.
True, but since this is public API, I think the redundancy is worth it for the sake of clarity.
} | ||
end = (end - removals) & mask | ||
} else { | ||
(0 until (idx + removals)).reverse foreach {i => |
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.
All these operations on ranges could get expensive. Might be more straightforward to use a while loop here anyway.
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.
|
||
@inline private def checkIndex(idx: Int, seq: GenSeq[_] = this) = if(!seq.isDefinedAt(idx)) throw new IndexOutOfBoundsException(idx.toString) | ||
|
||
@inline private def box(i: Int) = if (i <= 0) 0 else if (i >= size) size else i |
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.
Seems like the intention here is to find a location at which to slice, so maybe call this slicePoint
.
|
||
@inline private def checkIndex(idx: Int, seq: GenSeq[_] = this) = if(!seq.isDefinedAt(idx)) throw new IndexOutOfBoundsException(idx.toString) | ||
|
||
@inline private def box(i: Int) = if (i <= 0) 0 else if (i >= size) size else i |
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.
Also this could be written as i max 0 min size
. I don't have a strong opinion either way but if you're using max
and min
already you might as well use them consistently throughout the code.
Regarding the Travis failure https://travis-ci.org/scala/collection-strawman/builds/214811920, it's a StackOverflow on
|
I have a question: in the case of repeatedly removing an element from the end and adding an element to the beginning, is this still more performant than ArrayBuffer? Can you name any edge cases for performance? |
this.end = end | ||
} | ||
|
||
private def ensureCapacity() = if (size == array.length - 1) resize(array.length) |
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 will never reduce the array size. When size is less than 1/4 of array.length, you can halve the array size and keep the amortized O(1) costs.
That's subtle though: when size is only less than 1/2 of array.length, you can't halve the array size. Otherwise, if you take an array half full, you can add and remove elements to cause a reallocation every, say, two elements, and get amortized O(n) cost.
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.
ensureCapacity is used internally to grow the array to next powers of 2... The user must call trimToSize
to explicitly resize to a smaller size (same as in Java's ArrayDequeue). We dont want to resize all the time we do a add/remove.
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 agree we shouldn't reallocate all the time. But the trick I suggest comes from Cormen's algorithm book, which proves it "doesn't reallocate too often". Technically, it proves the amortized complexity (sort of the average complexity over operations, though not really) is still O(1), though the constant factors can be worse.
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.
OTOH, getting this right is a bit subtle as I mentioned. And if the Java code doesn't use this trick, I guess there might be tradeoffs, including the constant factors.
On the other hand, calling trimToSize too often decreases performance, not calling it wastes space, and calling it smartly... is often a complicated way of doing the same thing. I'll believe you can do better when I see it.
So I'd suggest to still auto-decrease space by default, and maybe have a flag for more manual management for more advanced scenarios, if there's a demonstrated use-case.
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 issue is that one might call clear() on a large buffer, then add a bunch of elements one at a time after that in order to avoid extra allocation. In that use case, having it shrink at being below 1/4 full is a bad idea. Another situation is if the user pre-sizes it to say, 2MB then starts adding things one at a time. We would not want it to shrink on the first element added in that case either. Nor would we want it to shrink if we added 100 things, removed 1, added 100, removed 1, etc (e.g. only checking on removal). It will take a long time before it reaches more than 1/4 full, and it is too hard to know inside of ensureCapacity()
what the user's intention is.
And since every append / prepend needs to ensureCapacity()
, it needs to be as simple and fast as possible for the job.
A separate maybeShrink()
applied selectively might work, but I'm not sure what the right heuristics for that would be -- retain the initial size? track a recent clear() call? That all seems messy to me.
I think if the api did not have a clear() and was purely append/prepend/removeTail/removeHead then auto-shrink at the right time is a better fit. I would go as far as to say that a structure that auto-shrinks is a different data structure entirely, with a slightly different use case. I'd rather have users ask for that by type, something like ShrinkingArrayDeque
. However, I can't think of a use case where trimToSize
is not good enough. Once I am done mutating the buffer, if I intend to hold on to it, I can call that.
} | ||
} | ||
|
||
override def length = (end - start) & mask |
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.
To call this method, this whole file calls size
— to find its implementation when reviewing ensureCapacity
I had to find override def size = length
here. Calling length
directly in the rest of the code would ease review.
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 thought it was pretty well known that size and length are interchangeable for all collections?
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.
Yes, but I saw size
, and when I found no definition I just deduced "it's from a superclass somehow" (I thought in Buffer
).
But this is certainly a small thing, and disappears with an IDE.
private[ArrayDeque] def alloc(len: Int) = { | ||
var i = len max defaultInitialSize | ||
//See: http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 | ||
i |= i >> 1; i |= i >> 2; i |= i >> 4; i |= i >> 8; i |= i >> 16 |
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 code omits the initial v--;
from the reference, yet after a while I confirmed it's correct. Took me a while to understand the code and confirm that this finds the first power of 2 > $len, while the original code finds the first power >= $len. It would be better to elaborate in comments, or this code will look suspicious to other reviewers.
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 typical way to implement "round to first power of 2 > len" is:
val nextPowerOf2gtLen = ((1 << 31) >>> Integer.numberOfLeadingZeros(len)) << 1
given that numberOfLeadingZeros
is one CPU instruction on most processors.
The graphics.standford.edu tricks are written mostly for C, which does not have a standard way to "invoke" the clz/lzcount CPU instruction.
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.
@Blaisorblade - the comment clearly says 2 > $len
on line 260! But good point @sjrd - will change. @sjrd - do you have a reference for "modern bithacks" like this?
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.
No, I don't really have a reference. It's mostly from experience implementing a lot of bit hackery for Scala.js. ;)
Oh interesting. That only happens as long as you don't have too many reallocations in that sequence. That's exceedingly tricky to verify, but after staring for a while at
|
PS @edmundnoble: haven't tested stuff, so I'm happy to be corrected. |
@Blaisorblade: Do you know why the scalac is throwing a SO here!? Also, yes the ensureCapacity is a bit hard to understand because it relies on 2 things: alloc returning > len (and not >= len) and we resizing when we are 1 element short and not when the cells are actually all full... I will document it in comments... |
I only highlighted there's an SO, I'm afraid I can't investigate it or even write a proper bug report. |
* @param destStart | ||
* @param maxItems | ||
*/ | ||
def arrayCopy(dest: Array[_], srcStart: Int, destStart: Int, maxItems: Int): Unit = { |
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.
Can we call this copyPartToArray
or something that better illustrates the difference from copyToArray
? Also, to match up with System.arraycopy
, the parameters should probably be in the order srcStart
, dest
, destStart
, maxItems
.
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.
Done!
build.sbt
Outdated
scalaVersion in ThisBuild := "2.12.2-ebe1180-SNAPSHOT" // from https://github.com/scala/scala/pull/5742 | ||
//resolvers in ThisBuild += "scala-pr" at "https://scala-ci.typesafe.com/artifactory/scala-pr-validation-snapshots" | ||
//scalaVersion in ThisBuild := "2.12.2-ebe1180-SNAPSHOT" // from https://github.com/scala/scala/pull/5742 | ||
scalaVersion in ThisBuild := "2.12.1" |
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.
Please drop this version change. scala/scala#5742 (comment) actually says that bugfix is needed here:
Rebased on top of 2.12.x head in order to build a version that also includes #5708, so I can use it in collection-strawman.
but dropping this version change from the PR is a good idea anyway.
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.
done: aa20b6c
@julienrf : Can you take a look at 7b2b9e1 ? It is my first attempt at following the Scala 2.13 collection style. Can you tell me if I am in the right direction? Got most of it to compile except 3 things:
If you have time, can you checkout this branch and see an easy way to fix the compile error? |
I had a quick look and that looks good. I’ll take more time to review tomorrow.
You can do a runtime test: myIterableOnce match {
case it: Iterable[A] => …
case _ => …
}
We have it: collection-strawman/collections/src/main/scala/strawman/collection/Iterable.scala Line 312 in 3c725f2
Otherwise, the generic version is |
@julienrf Do you have an overview what's still missing here? It would be nice to get this merged so I can drop the old |
No but I can review the PR for tomorrow. |
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.
Hey @pathikrit, thanks for your work, we still need to do a few changes before merging it, though. Can you also rebase on top of master (you will have to change add
and subtract
to addOne
and subtractOne
) and squash all the commits into a single one?
benchmark("Sliding", _.sliding(size = 1000, step = 1000).size) | ||
benchmark("Random indexing", b => range10m.foreach(i => if (b.isDefinedAt(i)) b(i))) | ||
benchmark("toArray", _.toArray) | ||
benchmark("Clear lots of items", _.clear()) |
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 agree that your code is more readable than JMH things but we should use JMH… Could you migrate your benchmark to JMH? You can just copy-paste another benchmark (e.g. the one for ArrayBuffer
) and change a few things to make it use ArrayDeque
.
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.
We can keep both for now..
appendAssumingCapacity(elem) | ||
} | ||
|
||
override def prepend(elem: A) = { |
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.
In general I prefer omitting the override
keyword when I only implement a member, to avoid accidentally overriding 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.
I prefer the other way - if I don't put override here, next time some removed the def from the super class, I would be left with a useless method but putting override here would be a compile error if someone removes this from super class
// The following code resizes the current collection atmost once and traverses elems atmost twice | ||
elems.knownSize match { | ||
// Size is too expensive to compute AND we can traverse it only once - can't do much but retry with an IndexedSeq | ||
case srcLength if srcLength < 0 && !elems.isTraversableAgain => elems.toIndexedSeq ++=: this |
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.
Instead of isTraversableAgain
you should pattern match on elems
:
elems match {
case iterable: Iterable[A] => // elems are travarsable again
case _ => // elems are traversable only once
}
* @param f the predicate used for choosing elements | ||
* @return | ||
*/ | ||
def removeHeadWhile(f: A => Boolean): strawman.collection.Seq[A] = { |
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.
Since we have take
and takeRight
, drop
and dropRight
, maybe we should follow the same convention here and have removeWhile
and removeRightWhile
?
* | ||
* @return | ||
*/ | ||
def peek: Option[A] = headOption |
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.
You should make it final
and @inline
and indicate in the documentation that it is equivalent to headOption
. BTW, is it really important to be able to write peek
instead of headOption
? I would stay with just headOption
since this is what we already have.
* @param destStart | ||
* @param maxItems | ||
*/ | ||
def copySliceToArray(srcStart: Int, dest: Array[_], destStart: Int, maxItems: Int): dest.type = { |
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.
Why don’t you indicate the type of the elements in the destination array?
def copySliceToArray[B >: A](srcStart: Int, dest: Array[B], destStart: Int, maxItems: Int): Array[B]
?
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 get this then:
[error] /Users/pbhowmick/Projects/collection-strawman/collections/src/main/scala/strawman/collection/mutable/ArrayDeque.scala:456: type mismatch;
[error] found : Array[AnyRef]
[error] required: Array[Any]
[error] Note: AnyRef <: Any, but class Array is invariant in type T.
[error] You may wish to investigate a wildcard type such as `_ <: Any`. (SLS 3.2.10)
[error] val array2 = copySliceToArray(srcStart = 0, dest = ArrayDeque.alloc(len), destStart = 0, maxItems = n)
[error] ^
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.
Yeah, I saw that too. BTW, have you seen my branch where I fixed all the compilation issues?
override def apply(idx: Int) = parent(idx) | ||
|
||
override def className = "ArrayDequeView" | ||
} |
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.
It seems to be exactly the same as the default IndexedView
instance for IndexedSeq
(except for the className
).
def newBuilder[A](): Builder[A, ArrayDeque[A]] = | ||
new GrowableBuilder[A, ArrayDeque[A]](empty) { | ||
override def sizeHint(size: Int): Unit = { | ||
//TODO: elems.ensureSize(size) |
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.
Time to uncomment this line :)
object ArrayDeque extends StrictOptimizedSeqFactory[ArrayDeque] { | ||
/** Avoid reallocation of buffer if length is known. */ | ||
def from[B](coll: collection.IterableOnce[B]): ArrayDeque[B] = | ||
empty[B].addAll(coll) //TODO: Can be optimized based on coll.size? |
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.
Yes, you can surely optimize when knownSize
is non negative (as we do in ArrayBuffer
).
@@ -0,0 +1,56 @@ | |||
package strawman | |||
package collection.test |
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.
It would be better to put these tests in the test/junit
sub-project.
while(nonEmpty) { | ||
elems += removeHeadAssumingNonEmpty() | ||
} | ||
elems.result() |
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 doesn’t seem to be the fastest way to perform this operation. You could copy all the elements in bulk to an ImmutableArray
.
Hey @pathikrit, I’ve rebased your work on master, fixed compilation errors and implemented a few missing things. However the tests don’t pass. For instance, edit: Actually the error was in |
@julienrf : Thank you so much! Sorry I got busy and did not get time - I will try to work on it this weekend! |
@pathikrit No worries. In the end that looks good! Remaining issues:
This gives me unusable charts like that one:
|
If I run your benchmarks then I get the following:
The result for “random indexing” looks weird but I’ve run the benchmark several times and always got results consistent with that one. |
Hmm, random indexing should be fast but this might be an artifact of us micro-benchmarking instead of a real suite. Also, the O(n) clear in ArrayBuffer is a bug right - it would cause memory the JVM to not GC and hold on to a possible large and expensive array that you just cleared. |
@pathikrit Any chance you can have a look at it soon? |
Oh just saw your branch - hah, I was progressing towards the same. Looks good - I myself was reaching similar code. Thanks for looking into it.
|
@julienrf : What's the next step on this? Can we merge your branch into this? Shall we just open a new clean state PR? |
You can just copy-paste my changes to your branch. |
Superseded by #490. |
Prior discussion:
https://contributors.scala-lang.org/t/using-circular-buffers-as-mutable-arraybuffer/454
SI-10167: https://issues.scala-lang.org/browse/SI-10167
ArrayDeque's (implemented using a growable circular buffers using modular arithmetic for indexing) are strictly better data structures than vanilla ArrayBuffers.
This improves on
java.util.ArrayDeque
by providing an O(1)get(idx)
and also providing fasterinsertAt(idx)
anddeleteAt(idx)
operations.This implementation adds
O(1)
append
,prepend
,removeHead
,removeTail
,get
,update
andclear
APIs.We also provide proof of concept replacement implementations of mutable data structures like stack, queues and buffers with ArrayDeque here.
Preliminary benchmarks prove that this data structure can suitably replace
mutable.ArrayBuffer