-
Notifications
You must be signed in to change notification settings - Fork 613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simplify Chunk #2181
Simplify Chunk #2181
Conversation
…ak-sonatype-0.19.2 Update sbt-spiewak-sonatype to 0.19.2
…drop KnownElementType, consolidate array types
Note, I tried adding a rope like balanced binary tree implementation of private case class Concat[+O](left: Chunk[O], right: Chunk[O]) extends Chunk[O] {
def size: Int = left.size + right.size
def apply(i: Int): O =
if (i < left.size) left(i)
else right(i - left.size)
def copyToArray[O2 >: O](xs: Array[O2], start: Int): Unit = {
left.copyToArray(xs, start)
right.copyToArray(xs, start + left.size)
}
protected def splitAtChunk_(n: Int): (Chunk[O], Chunk[O]) =
if (n < left.size) {
val (pfx, sfx) = left.splitAtChunk_(n)
(pfx, sfx ++ right)
} else {
val (pfx, sfx) = right.splitAtChunk_(n - left.size)
(left ++ pfx, sfx)
}
override def ++[O2 >: O](that: Chunk[O2]): Chunk[O2] =
if (that.isEmpty) this
else if (isEmpty) that
else {
@annotation.tailrec
def go(node: Concat[O2], last: Chunk[O2]): Chunk[O2] = {
val lastN = last.size
if (lastN >= node.size || lastN * 2 <= node.right.size)
Concat(node, last)
else
node.left match {
case left: Concat[O2] => go(left, Concat(node.right, last))
case _ => Concat(node, last)
}
}
go(this, that)
}
} |
Do you mind expanding a bit more on how this avoids EDIT: Nevermind, got it I think. ArraySlice works for primitives (for which there is a ClassTag), ChunkQueue works for the rest, boxes, and requires no class tags. I wonder if that can result in boxing if a polymorphic combinator gets instantiated to a primitive, but I haven't fully thought it through |
I'm going to update the main ticket description with a summary of the changes, especially as there's migration work needed for folks that were using Couple of main themes:
|
But this requires a ClassTag bound, right? (if you're being generic on the chunk contents, which would hopefully happen rarely when combined with the other constraints above) |
Right, three choices:
While (2) can cause performance issues when working with singleton chunks, doing so already causes performance problems in fs2 -- the interpreter overhead ends up as dominating factor. |
I see, that makes sense. It would be good to get some of this in the scaladoc as well, once you're done coding it up. The current one still mentions individual specialised subtypes of Chunk |
Docs and PR description updated. This one is ready for review. |
/** Returns the a chunk which consists of the elements of this chunk and the elements of | ||
* the supplied chunk. This operation is amortized O(1). | ||
*/ | ||
def ++[O2 >: O](that: Chunk[O2]): Chunk[O2] = |
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.
😍
go(chunks, i) | ||
} | ||
|
||
override def take(n: Int): Queue[O] = |
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.
An interesting fact
According to the results of this benchmark
package fs2
package benchmark
import scala.util.Random
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import org.openjdk.jmh.annotations.{Benchmark, Param, Scope, Setup, State}
@State(Scope.Thread)
class ChunkQueueBenchmark {
@Param(Array("10", "100", "10000"))
var chunkSize: Int = _
var chunk: Chunk.Queue[Int] = _
@Setup
def setup(): Unit = {
val seq = Seq.fill(100_000/chunkSize)(Chunk.seq(Seq.fill(chunkSize)(Random.nextInt())))
chunk = Chunk.Queue(seq: _*)
}
@Benchmark
def drop(): Unit =
chunk.drop(chunkSize - 1)
@Benchmark
def take(): Unit =
chunk.take(chunkSize - 1)
}
drop
doesn't degrade when increasing SIZE of chunks, while take
does.
[info] ChunkQueueBenchmark.drop 10 thrpt 21521956,531 ops/s
[info] ChunkQueueBenchmark.drop 100 thrpt 21206792,447 ops/s
[info] ChunkQueueBenchmark.drop 10000 thrpt 19296012,270 ops/s
[info] ChunkQueueBenchmark.take 10 thrpt 13121396,574 ops/s
[info] ChunkQueueBenchmark.take 100 thrpt 2185148,679 ops/s
[info] ChunkQueueBenchmark.take 10000 thrpt 21255,437 ops/s
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.
How about chunk.drop(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.
oh yes, I messed up the benchmark, I used chunkSize - 1
instead of 100_000 - 1
thanks! 😳
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 previously thought, that appending to the end of immutable queue is linear. In take(n)
elements are appended, thus I thought that it would be n^2 to the amount of chunks. However, I was wrong. TIL, that appending to both the end and the head of the immutable queue in scala takes O(1), as does head(pop first) and dequeue(pop last).
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.
scala.collection.immutable.Queue
doesn't get nearly enough credit :)
Fixes the bug found in #2180, where
Chunk.concat
throws on Scala.js as a result of1.isInstanceOf[Byte]
returning true.This PR makes a number of simplifications:
Chunk.concat
now takes an implicitClassTag
and always instantiates an array via that class tag. If passing aClassTag[Byte]
, you get aChunk
backed by a primitive byte array. No more trickery withKnownElementType
and runtime type checks on chunk contents. As a result of this change, use sites ofconcat
which are polymorphic in the element type need aClassTag
.Chunk.Queue
is now a subtype ofChunk
and thetoChunk
method (which calledconcat
) is gone. In its place, thecompact
method was added toChunk
, which returns aChunk.ArraySlice
.compact
requires aClassTag
as well. Previously, chunks would be accumulated in to aChunk.Queue
and then copied to aArray[Any]
via the final call totoChunk
. Now, theChunk.Queue
is returned directly, avoiding the need to do any copying. As a result, pulls that usedChunk.Queue
to accumulate and thenPull.output(acc.toChunk)
must be modified to accumulate aChunk[O]
via++
and thenPull.output(acc)
.Chunk
now has an O(1)++
method, which usesChunk.Queue
underneath.Chunk.Ints
,Chunk.Bytes
-- were all removed as they are now redundant with the singleArraySlice
. One caveat: the primitive array chunks allowed unboxed indexed access via.at(idx)
, but this wasn't used in practice and folks instead should just access the underlying array directly.ByteBuffer
andCharBuffer
were kept due to their frequent use in Java NIO APIs.