Avoid allocations of reusable CanBuildFroms#8467
Conversation
c5ff417 to
f8b5889
Compare
src/library/scala/Array.scala
Outdated
| implicit def canBuildFrom[T](implicit t: ClassTag[T]): CanBuildFrom[Array[_], T, Array[T]] = | ||
| implicit def canBuildFrom[T](implicit t: ClassTag[T]): CanBuildFrom[Array[_], T, Array[T]] = { | ||
| val tag = implicitly[ClassTag[T]] | ||
| (tag.runtimeClass match { |
There was a problem hiding this comment.
Is this match really an improvement on the JVM? Because at least on JS this is going to be detrimental.
There was a problem hiding this comment.
its a saving on the allocation of the CanBuildFrom, not the CPU
There was a problem hiding this comment.
previously a use of a CBF would switch on the runtime class twice for each array, this changes it it 3 times. If its really an issue then we can specialise the code for each case and reduce to calls to one for each usage
what is the measured effect on JS? If so its already an issue
There was a problem hiding this comment.
Well in JS de can entirely stack allocate the CanBuildFrom and the ArrayBuilder down to completely eliminating them and constant-folding the ClassTags. We can do that because they are new instances. If they first switch to fetch an existing instance on the heap, we don't know what's in the heap and we can't constant-fold its members.
But I realized that we already override Array.scala with our own implementation to achieve this. So I guess the change in this PR won't affect us. At least not beyond the fact that it widens the gap between the JVM and the JS implementation of the library.
There was a problem hiding this comment.
Also, on the JVM, it is best to first switch on tag.runtimeClass.isPrimitive to have a fast path for non-primitive cases.
There was a problem hiding this comment.
@sjrd I'd be open to keeping an private[scala] final val isScalaJS = false constant in the library if that would help you avoid copy/paste. You could recompile with that constant flipped and get the original code here, for example.
There was a problem hiding this comment.
I did consider the isPrimitive check, but wanted to discuss.
Will rework, but it should also apply in other cases. There seem to be 9 locations to apply this
| implicit def canBuildFrom[T](implicit m: ClassTag[T]): CanBuildFrom[WrappedArray[_], T, WrappedArray[T]] = | ||
| implicit def canBuildFrom[T](implicit m: ClassTag[T]): CanBuildFrom[WrappedArray[_], T, WrappedArray[T]] = { | ||
| val tag = implicitly[ClassTag[T]] | ||
| (tag.runtimeClass match { |
There was a problem hiding this comment.
Same question: is this really an improvement?
969337c to
9e956b8
Compare
(For 2.12.x's eyes only.)
Use vals to cache a single instance of stateless CanBuildFrom instances.
These are cast by the existing `implicit def` to the suitable generic
type. This pattern was already used in some places -- this PR applies
it systematically across `collection.{mutable,immutable}`.
The `CanBuildFrom` instances for arrays and wrapped arrays are
cached for each primitive type, Unit, and Object. Each of these
instances is backed by a dedicated subclass of CanBuildFrom that
avoids subsequent dispatch on the `ClassTag[T]`.
58fb28d to
b6ba518
Compare
The with AnyRef idiom avoids a cast.
retronym
left a comment
There was a problem hiding this comment.
Approving of the idea. We should get another set of 👀 to review this as I was involved in the code change as well.
hrhino
left a comment
There was a problem hiding this comment.
Looks great! I'd like Stefan's opinion, too.
|
@retronym thanks for progressing this PR ⭐️ |
|
paging @szeiger for a look at this one. I'm happy with the PR myself. |
7935399 to
89bff62
Compare
order matches in expected frequency order (Array, WrappedArray + associated builders, and ClassTag.newArray) avoid extra def in BitSets don't optimise for NoBuilder cases
89bff62 to
18bf349
Compare
szeiger
left a comment
There was a problem hiding this comment.
Other than the remaining unnecessary forwarders this looks good.
| /** $bitsetCanBuildFrom */ | ||
| implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = bitsetCanBuildFrom | ||
| implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = ReusableCBF | ||
| private[this] val ReusableCBF = bitsetCanBuildFrom |
There was a problem hiding this comment.
Here's another case where we don't need a new val. All monomorphic ones can use val canBuildFrom.
| implicit def canBuildFrom: CanBuildFrom[WrappedString, Char, WrappedString] = | ||
| ReusableCBF.asInstanceOf[CanBuildFrom[WrappedString, Char, WrappedString]] | ||
| private[this] val ReusableCBF = new CanBuildFrom[WrappedString, Char, WrappedString] { | ||
| def apply(from: WrappedString) = newBuilder |
| /** $bitsetCanBuildFrom */ | ||
| implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = bitsetCanBuildFrom | ||
| implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = ReusableCBF | ||
| private[this] val ReusableCBF = bitsetCanBuildFrom |
use direct vals where no casting is required
(For 2.12.x's eyes only.)
Use vals to cache a single instance of stateless CanBuildFrom instances.
These are cast by the existing
implicit defto the suitable generictype. This pattern was already used in some places -- this PR applies
it systematically across
collection.{mutable,immutable}.The
CanBuildFrominstances for arrays and wrapped arrays arecached for each primitive type, Unit, and Object. Each of these
instances is backed by a dedicated subclass of CanBuildFrom that
avoids subsequent dispatch on the
ClassTag[T].