-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Changed HashMap.getOrElseUpdate to only calculate the index once #5528
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
Conversation
Signed the Scala CLA |
@@ -72,6 +72,17 @@ extends AbstractMap[A, B] | |||
else Some(e.value) | |||
} | |||
|
|||
override def getOrElseUpdate(key: A, defaultValue: => B): 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.
In case the element was missing from the map, the hash code (with some added variance) was calculated first for verification, and again for the put
operation.
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 not sure this is a binary-compatible change. @retronym - is this? I would not intuitively have thought it would be with the new trait encoding, but I'm not sure of the new 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.
@Ichoran It should be binary compatible. A class gets a forwarder method that delegates to the static implementation method of the interface. Adding an override merely replaces the implementation of the class method without any changes to the signatures. I'll add a proper test case for this to MiMa.
@@ -131,7 +131,7 @@ trait HashTable[A, Entry >: Null <: HashEntry[A, Entry]] extends HashTable.HashU | |||
protected def findEntry(key: A): Entry = | |||
findEntry0(key, index(elemHashCode(key))) | |||
|
|||
private[this] def findEntry0(key: A, h: Int): Entry = { | |||
protected[this] final def findEntry0(key: A, h: Int): Entry = { |
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.
These two parent methods needed to be accessed from HashMap
to avoid calculation the hash code twice.
final
was added to keep the semantics, changing only the visibility of the method.
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 not 100% sure this change is forwards binary compatible. A custom subclass of 2.12.1 hashtable might rely on this newly exposed API and incur a linkage error if run with the 2.12.0 standard library. Our build is supposed to check for such violations, but it hasn't flagged this problem. I need to check out if there is a problem in our build, or in MiMa itself. This won't be a blocker for the change as we can always copy/paste these private methods into HashMap
for the 2.12.x releases.
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 also be see whether performance of other methods in mutable.HashMap
could be improved by using copy/pasted implementations of findEntry
etc rather than those inherited from the HashTable
trait. Obviously this isn't an ideal solution, but HashMap
is so widely used, and these methods are so tiny that failure to JIT inline can have a real impact of observable performance, that I'd be open to this solution for 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.
My fix isn't about the inlining limit, that's a side-effect of collapsing the stack depth by calling the indexing methods earlier.
This fix would speed up 2.11 also.
Please advise on how you want me to change this PR.
I could start by adding a JMH benchmark for HashMap
and verifying the performance of other methods also - and trying to optimize them on the way.
Are we sure we want to do the benchmarks from Scala
and not Java
?
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.
@retronym, we would be forced to include more than these two methods, if we don't want to make it protected
:/
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 you inline findEntry0 and addEntry0 you'll end up with entirely protected methods, I think.
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.
@retronym I agree about the binary compatibility. I'll check MiMa for a test case. I'd be surprised if this isn't covered or doesn't work. Could indeed be a problem of the Scala build.
Review by @Ichoran |
The benchmark could be added to our (recently added) JMH suite, in the same place as OpenHashMapBenchmark. We don't run these automatically, but it will at least provide a reference for someone studying this commit or making further changes to this area. The benchmark itself could do with tests of workloads that exercise more code paths. For instance, using different key types will lead control through through more paths in |
7086725
to
47cffd3
Compare
The benchmarks require first building Scala into `../../build/pack` with `ant`. | ||
If you want to build with `sbt dist/mkPack` instead, | ||
you'll need to change `scalaHome` in this project. | ||
The benchmarks require first building Scala into `../../build/pack`. |
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.
sbt
does it correctly also, no need for ant
anmore
@@ -1,2 +1,2 @@ | |||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") | |||
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.16") | |||
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.17") |
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.
Sent a PR to update SBT-JMH, it was accepted and the new version containing the latest JMH
was included here
} | ||
} | ||
|
||
@Benchmark def getOrElseUpdate_missing(bh: Blackhole): 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.
these two (found and not found in map) should probably be separated, as they have different speeds.
Otherwise the result would depend on their cardinality's ratio.
@@ -131,7 +131,7 @@ trait HashTable[A, Entry >: Null <: HashEntry[A, Entry]] extends HashTable.HashU | |||
protected def findEntry(key: A): Entry = | |||
findEntry0(key, index(elemHashCode(key))) | |||
|
|||
private[this] def findEntry0(key: A, h: Int): Entry = { | |||
protected[this] final def findEntry0(key: A, h: Int): Entry = { |
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.
@retronym, we would be forced to include more than these two methods, if we don't want to make it protected
:/
@Setup(Level.Trial) def initKeys(): Unit = { | ||
values = (0 to size).map(i => (i % 10) match { | ||
case 0 => i.toString | ||
case 1 => i.toByte |
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 excessive. Object and non-object are different paths, and maybe constant-box vs. newly allocated box (byte vs double) is good to check. Having to worry about so many seems excessive.
addEntry0(newEntry, i) | ||
newEntry.value | ||
} | ||
} |
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 implementation looks fine, but what about using findOrAddEntry
in HashTable
? Does that still show the same speed improvement?
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 idea is very good, but we don't have a value
in HashTable
's Entry
, only key
and next
.
The other alternative would be to call findOrAddEntry
directly, but then defaultValue
would be evaluated twice, which should probably be avoided, right?
override def getOrElseUpdate(key: A, defaultValue: => B): B = {
val e = findOrAddEntry(key, defaultValue)
if (e ne null) e.value else defaultValue
}
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.
which should probably be avoided, right?
Correct. Also, we mustn't evaluate defaultValue
if the key already exists.
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.
Oops, didn't read the signatures correctly. It's not evaluating twice which is a problem; it's evaluating more than zero times when the entry exists. You're right; we can't do it this way.
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.
Thanks!
In this particular case I think it cannot be evaluated once, only zero or two times (doesn't change the fact).
4dac2b0
to
e37b62e
Compare
@paplorinc - As an aside: I'm not sure why using JMH from Scala rather than Java is "counter-intuitive" for you. A benchmark that depends on what language you use to embed it is almost by definition a bad benchmark because it means that the incidental stuff that you are not supposed to care about is not negligibly inexpensive compared to what you're actually interested in. |
I think the core question that I don't have an answer to is whether we're allowed to introduce an override to getOrElseUpdate and still stay binary compatible. If yes, then I think the other issues can be worked around by duplicating code (inlining it manually) until you end up with a set of calls to protected or public methods. If yes in both 2.12 and 2.11, maybe this patch could be backported! Faster hash maps benefit almost everyone. |
This is okay because the class already inherits a method with and identical signature from Here's an example of making this class change with Java code without linkage errors: https://gist.github.com/794c9f88771b210f5dab0b65793b4682 Here's the MiMa test case that shows that deleting an exact override is backwards compatible (meaning adding an exact override, like we do here, is forward compatible!) |
@retronym - Okay, then I think we're clear once @paplorinc switches to use |
@Ichoran, to be sure I understand what you're requesting, do you want me to keep copy-pasting things from override def getOrElseUpdate(key: A, defaultValue: => B): B = {
val i = index(elemHashCode(key))
val entry = findEntry0(key, i)
if (entry != null)
entry.value
else {
val newEntry = createNewEntry(key, defaultValue)
addEntry0(newEntry, i)
newEntry.value
}
}
private[this] final def findEntry0(key: A, h: Int): Entry = {
var e = table(h).asInstanceOf[Entry]
while (e != null && !elemEquals(e.key, key)) e = e.next
e
}
private[this] final def addEntry0(e: Entry, h: Int) {
e.next = table(h).asInstanceOf[Entry]
table(h) = e
tableSize = tableSize + 1
nnSizeMapAdd(h)
if (tableSize > threshold)
resize(2 * table.length)
}
private def resize(newSize: Int) {
val oldTable = table
table = new Array(newSize)
nnSizeMapReset(table.length)
var i = oldTable.length - 1
while (i >= 0) {
var e = oldTable(i)
while (e != null) {
val h = index(elemHashCode(e.key))
val e1 = e.next
e.next = table(h).asInstanceOf[Entry]
table(h) = e
e = e1
nnSizeMapAdd(h)
}
i = i - 1
}
threshold = newThreshold(_loadFactor, newSize)
}
private[collection] final def newThreshold(_loadFactor: Int, size: Int) = ((size.toLong * _loadFactor) / loadFactorDenom).toInt
private[collection] final def loadFactorDenom = 1000 edit: about the Scala JMH tests (which I provided upon request), my concern is that (using a metaphor) you cannot trust a patient to self-diagnose an illness. i.e. you could use sbt-jmh for non-scala-core (i.e. when Scala is "healthy"), but for inside problems, you might be relying on the problem itself for diagnosing it (i.e if the compiler emits wrong bytecode, the benchmark itself will contain that error). edit2: I could extract some logic to stateless methods in edit3: I could annotate the two exposed methods with |
@paplorinc - Which fields do you need? You've got everything already there as protected, don't you? It might be easier to intercept the size and just eat the extra lookup on an addEntry in the relatively rare case that the table needs to be enlarged. I grant that it's ugly either way, but if we can avoid even a minor binary incompatibility, I think it's for the best. If we'd caught this before 2.12 was out, I'd have much preferred your patch as it was where you just change the visibility of some things. Also, I understand your point in general regarding using a flawed system to detect its own flaws, but in this particular case, do you also understand my point? |
The binary incompatibility wasn't reported because of a regression in the entry point into MiMa we use. Will be fixed tomorrow: lightbend-labs/mima#138 |
e37b62e
to
df50ce7
Compare
@Ichoran, thanks for the idea, I've tested it from the new Please wait with the merge until I validate the results from @retronym, thank you for your investigations! |
Could you please add comments in both copies of each duplicated method to warn maintainers about the duplication? For bonus points/pints, you could submit a PR to the 2.13.x branch to make them protected and shared. |
df50ce7
to
8b383cf
Compare
e = e.next | ||
e | ||
} | ||
private[this] def notFound(key: A, e: Entry): Boolean = (e != null) && !elemEquals(e.key, key) |
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.
extracted to make findEntry
inlinable (and the method more readable):
scala.collection.mutable.HashMap::findEntry (32 bytes) inline (hot)
private[this] def notFound(key: A, e: Entry): Boolean = (e != null) && !elemEquals(e.key, key) | ||
|
||
/* inlined HashTable.addEntry0 to preserve its visibility */ | ||
private[this] def addEntry(e: Entry, h: Int): 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.
scala.collection.mutable.HashMap::addEntry (30 bytes) inline (hot)
@@ -72,6 +72,37 @@ extends AbstractMap[A, B] | |||
else Some(e.value) | |||
} | |||
|
|||
override def getOrElseUpdate(key: A, defaultValue: => B): 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.
scala.collection.mutable.HashMap::getOrElseUpdate (48 bytes) inline (hot)
@@ -365,9 +365,8 @@ trait HashTable[A, Entry >: Null <: HashEntry[A, Entry]] extends HashTable.HashU | |||
// this is of crucial importance when populating the table in parallel | |||
protected final def index(hcode: 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.
scala.collection.mutable.HashTable::index (33 bytes) inline (hot)
val improved = improve(hcode, seedvalue) | ||
val shifted = (improved >> (32 - java.lang.Integer.bitCount(ones))) & ones | ||
shifted | ||
val exponent = Integer.numberOfLeadingZeros(ones) |
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 table.length
is a power of two, table.length - 1
is composed of ones. The bitCount of that is the number of ones (= the exponent of the original power of 2), subtracted from 32 is the number of trailing zeroes.
The benchmark indicates that this is smaller and faster:
public static class Test {
final int[] squares = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824};
@Benchmark
public void bitCount(Blackhole bh) {
for (int i = 0; i < squares.length; i++) {
int leadingZeros = Integer.SIZE - Integer.bitCount(squares[i] - 1);
bh.consume(leadingZeros);
}
}
@Benchmark
public void numberOfLeadingZeros(Blackhole bh) {
for (int i = 0; i < squares.length; i++) {
int leadingZeros = Integer.numberOfLeadingZeros(squares[i] - 1);
bh.consume(leadingZeros);
}
}
}
Benchmark Score Error Units
Test.bitCount 8746665.311 ± 206703.549 ops/s
Test.numberOfLeadingZeros 9255031.968 ± 114967.712 ops/s
(ops/s
, greater is better)
Note: this affect all hashing operations!
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.
Speed improvement or no, when the goal is to count the number of leading zeros it's better to clean up the method and say numberOfLeadingZeros
unless there's a significant performance hit. It's nice that it's faster too, though!
|
||
return x.equals(y); | ||
private static boolean equalsNotSame(Object x, Object y) { | ||
return x != null && y != null && equalsNotSameOrNull(x, y); |
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 they're not the same (==
), but one of them is null
, they're certainly not equal
(scala.runtime.BoxesRunTime::equalsNotSame (22 bytes) inline (hot)
)
return equalsNumChar(xn, (java.lang.Character)y); | ||
if (xn == null) | ||
return y == null; | ||
private static boolean equalsNotSameOrNull(Object x, Object y) { |
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.runtime.BoxesRunTime::equalsNotSameOrNull (38 bytes) inline (hot)
var keys: Vector[Any] = _ | ||
|
||
@Setup(Level.Trial) def initKeys(): Unit = { | ||
keys = (0 to size).map(i => (i % 4) match { |
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, reduced the number of possibilities, hope it's ok now :)
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 reasonable, thanks :)
…ethod As questioned in scala/scala#5528
8b383cf
to
ab02d44
Compare
|
||
return xn.equals(y); | ||
static boolean equalsNumObject(java.lang.Number xn, Object y) { |
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't change signatures or accessibility of these methods. Even after the #5532, our binary compatibility checker didn't pick up this problem. I've patched the tool in lightbend-labs/mima#142 to report these errors.
Beyond the constraint that we can't change signatures, we also can't change semantics of of these "building blocks" of boxed equality, because the compiler emits direct calls to a variety of them, depending on how much static knowledge we have about the types and nullability of the arguments.
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 is safe to introduce new private methods, and to refactor existing methods to use those private methods, so long at the semantics of existing non-private methods is unchanged.
I'd suggest to split out changes to BoxesRuntime
into a separate commit and present benchmark results. We need to weigh up the performance wins against the risk of introducing regressions or subtle compatibility problems when mixing library from 2.12.1 with code compiled with 2.12.0 and vice versa.
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.
My patched version of MiMa reports:
Found 5 binary incompatibilities
================================
* method equalsNumObject(java.lang.Number,java.lang.Object)Boolean in class
scala.runtime.BoxesRunTime is inaccessible in current version, it must be
public.
* method equals2(java.lang.Object,java.lang.Object)Boolean in class
scala.runtime.BoxesRunTime does not have a correspondent in current version
* method equalsCharObject(java.lang.Character,java.lang.Object)Boolean in
class scala.runtime.BoxesRunTime does not have a correspondent in current
version
* method equalsNumNum(java.lang.Number,java.lang.Number)Boolean in class
scala.runtime.BoxesRunTime is inaccessible in current version, it must be
public.
* method equalsNumChar(java.lang.Number,java.lang.Character)Boolean in
class scala.runtime.BoxesRunTime does not have a correspondent in current
version
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.
Thanks @retronym, will split it :)
5cc7418
to
b1c83ab
Compare
@@ -18,8 +16,7 @@ Using this example, one would simply run | |||
|
|||
jmh:runMain scala.collection.mutable.OpenHashMapRunner |
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.
not sure how to run only a single benchmark, sbt jmh:run
runs everything, even if I provide a pattern after it :/
} | ||
} | ||
|
||
@Benchmark def get(bh: Blackhole): 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.
added get
and put
also, even though they weren't affected by this commit
var map = new mutable.HashMap[Any, Any] | ||
|
||
var i = 0; | ||
while (i < 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.
changed for
comprehension to while
for better SNR
Fixes https://issues.scala-lang.org/browse/SI-10049 Since `groupBy` uses this method extensively and suffered a measurable slowdown in `2.12.0`, this modification restores (and exceeds) its original speed. --- included benchmarks: (`ns/op` → smaller is better) `before (2.12.0):` ```java Benchmark (size) Mode Cnt Score Error Units s.c.immutable.VectorMapBenchmark.groupBy 10 avgt 20 865.693 ± 7.869 ns/op s.c.immutable.VectorMapBenchmark.groupBy 100 avgt 20 3095.657 ± 56.438 ns/op s.c.immutable.VectorMapBenchmark.groupBy 1000 avgt 20 28247.005 ± 470.513 ns/op s.c.mutable.HashMapBenchmark.get 10 avgt 20 679.448 ± 11.809 ns/op s.c.mutable.HashMapBenchmark.get 100 avgt 20 7240.178 ± 61.734 ns/op s.c.mutable.HashMapBenchmark.get 1000 avgt 20 95725.127 ± 2373.458 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 10 avgt 20 836.561 ± 20.085 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 100 avgt 20 7891.368 ± 56.808 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 1000 avgt 20 97478.629 ± 1782.497 ns/op s.c.mutable.HashMapBenchmark.put 10 avgt 20 243.422 ± 2.915 ns/op s.c.mutable.HashMapBenchmark.put 100 avgt 20 5810.927 ± 60.054 ns/op s.c.mutable.HashMapBenchmark.put 1000 avgt 20 82175.539 ± 1690.296 ns/op ``` `after:` ```java Benchmark (size) Mode Cnt Score Error Units s.c.immutable.VectorMapBenchmark.groupBy 10 avgt 20 627.007 ± 9.718 ns/op s.c.immutable.VectorMapBenchmark.groupBy 100 avgt 20 2086.955 ± 19.042 ns/op s.c.immutable.VectorMapBenchmark.groupBy 1000 avgt 20 19515.234 ± 173.647 ns/op s.c.mutable.HashMapBenchmark.get 10 avgt 20 683.977 ± 11.843 ns/op s.c.mutable.HashMapBenchmark.get 100 avgt 20 7345.675 ± 41.092 ns/op s.c.mutable.HashMapBenchmark.get 1000 avgt 20 95085.926 ± 1702.997 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 10 avgt 20 503.208 ± 2.643 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 100 avgt 20 5526.483 ± 28.262 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 1000 avgt 20 69265.900 ± 674.958 ns/op s.c.mutable.HashMapBenchmark.put 10 avgt 20 252.481 ± 7.597 ns/op s.c.mutable.HashMapBenchmark.put 100 avgt 20 5708.034 ± 110.360 ns/op s.c.mutable.HashMapBenchmark.put 1000 avgt 20 82051.378 ± 1432.009 ns/op ``` i.e. for the given benchmark conditions `~40%` faster `groupBy` and `getOrElseUpdate`
b1c83ab
to
b67ca7d
Compare
@Ichoran, not sure why the build is failing do I still have a |
|
This looks like an unrelated failure due to MiMa having been accidentally disabled for a while. |
@szeiger, what should I do to fix it (tried rebasing and retriggering the build)? |
/nothingtoseehere The failure is due to #5481, which was based on an older commit before #5532, so the problem was not detected. There is nothing to do here. Since the binary incompatibility is in a private implementation class I would argue that we should whitelist it. @retronym, do you agree? The other failure in the new build is a spurious error that we've seen before. |
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.
LGTM
LGTM, too. This is a fantastic contribution, keep them coming! |
Fixes https://issues.scala-lang.org/browse/SI-10049
Since
groupBy
uses this method extensively and suffered a measurable slowdown in2.12.0
, this modification restores (and exceeds) its original speed.included benchmarks:
(
ns/op
→ smaller is better)before (2.12.0):
after:
i.e. for the given benchmark conditions
~40%
fastergroupBy
andgetOrElseUpdate