Skip to content

Avoid allocations of reusable CanBuildFroms#8467

Merged
szeiger merged 4 commits intoscala:2.12.xfrom
rorygraves:mike/2.12_CanBuildFrom
Dec 5, 2019
Merged

Avoid allocations of reusable CanBuildFroms#8467
szeiger merged 4 commits intoscala:2.12.xfrom
rorygraves:mike/2.12_CanBuildFrom

Conversation

@mkeskells
Copy link
Contributor

@mkeskells mkeskells commented Oct 14, 2019

(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].

@scala-jenkins scala-jenkins added this to the 2.12.11 milestone Oct 14, 2019
hrhino
hrhino previously requested changes Oct 14, 2019
@mkeskells mkeskells force-pushed the mike/2.12_CanBuildFrom branch from c5ff417 to f8b5889 Compare October 14, 2019 21:43
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this match really an improvement on the JVM? Because at least on JS this is going to be detrimental.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its a saving on the allocation of the CanBuildFrom, not the CPU

Copy link
Contributor Author

@mkeskells mkeskells Oct 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, on the JVM, it is best to first switch on tag.runtimeClass.isPrimitive to have a fast path for non-primitive cases.

Copy link
Member

@retronym retronym Oct 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that @retronym fixed these

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: is this really an improvement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same answer

@retronym retronym force-pushed the mike/2.12_CanBuildFrom branch 3 times, most recently from 969337c to 9e956b8 Compare October 21, 2019 04:50
(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]`.
@retronym retronym force-pushed the mike/2.12_CanBuildFrom branch from 58fb28d to b6ba518 Compare October 31, 2019 00:58
@retronym retronym changed the title use val for CanBuildFrom where possible Avoid allocations of reusable CanBuildFroms Oct 31, 2019
@retronym retronym dismissed hrhino’s stale review October 31, 2019 00:59

The with AnyRef idiom avoids a cast.

Copy link
Member

@retronym retronym left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving of the idea. We should get another set of 👀 to review this as I was involved in the code change as well.

@retronym retronym requested a review from szeiger October 31, 2019 01:01
Copy link
Contributor

@hrhino hrhino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I'd like Stefan's opinion, too.

@mkeskells
Copy link
Contributor Author

@retronym thanks for progressing this PR ⭐️

@diesalbla diesalbla added the library:collections PRs involving changes to the standard collection library label Oct 31, 2019
@SethTisue SethTisue added the performance:do_not_allocate Changes to avoid object allocations label Nov 11, 2019
@retronym
Copy link
Member

paging @szeiger for a look at this one. I'm happy with the PR myself.

@mkeskells mkeskells force-pushed the mike/2.12_CanBuildFrom branch 2 times, most recently from 7935399 to 89bff62 Compare December 1, 2019 22:54
order matches in expected frequency order (Array, WrappedArray + associated builders, and ClassTag.newArray)
avoid extra def in BitSets
don't optimise for NoBuilder cases
@mkeskells mkeskells force-pushed the mike/2.12_CanBuildFrom branch from 89bff62 to 18bf349 Compare December 1, 2019 23:09
Copy link
Contributor

@szeiger szeiger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another case where we don't need a new val. All monomorphic ones can use val canBuildFrom.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one, too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/** $bitsetCanBuildFrom */
implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = bitsetCanBuildFrom
implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = ReusableCBF
private[this] val ReusableCBF = bitsetCanBuildFrom
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

use direct vals where no casting is required
@szeiger szeiger merged commit de3451d into scala:2.12.x Dec 5, 2019
@lrytz lrytz added performance the need for speed. usually compiler performance, sometimes runtime performance. and removed performance:do_not_allocate Changes to avoid object allocations labels Mar 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

library:collections PRs involving changes to the standard collection library performance the need for speed. usually compiler performance, sometimes runtime performance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants