8000 Changed HashMap.getOrElseUpdate to only calculate the index once by l0rinc · Pull Request #5528 · scala/scala · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
merged 3 commits into from
Nov 22, 2016

Conversation

l0rinc
Copy link
Contributor
@l0rinc l0rinc commented Nov 14, 2016

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):

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

after:

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

i.e. for the given benchmark conditions ~40% faster groupBy and getOrElseUpdate

@scala-jenkins scala-jenkins added this to the 2.12.2 milestone Nov 14, 2016
@SethTisue SethTisue modified the milestones: 2.12.1, 2.12.2 Nov 14, 2016
@l0rinc
Copy link
Contributor Author
l0rinc commented Nov 14, 2016

Signed the Scala CLA

@@ -72,6 +72,17 @@ extends AbstractMap[A, B]
else Some(e.value)
}

override def getOrElseUpdate(key: A, defaultValue: => B): B = {
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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 = {
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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 :/

Copy link
Contributor

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.

Copy link
Contributor

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.

@adriaanm
Copy link
Contributor

Review by @Ichoran

@retronym
Copy link
Member

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 BoxesRuntime.{equals*, hash*}, and different function arguments to produce the default to make the call to Function.apply within getOrElseUpdate megamorphic.

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`.
Copy link
Contributor Author

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")
Copy link
Contributor Author
@l0rinc l0rinc Nov 15, 2016

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 = {
Copy link
Contributor Author

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 = {
Copy link
Contributor Author

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

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

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?

Copy link
Contributor Author

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
}

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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

@l0rinc l0rinc force-pushed the getOrElseUpdate branch 3 times, most recently from 4dac2b0 to e37b62e Compare November 15, 2016 22:36
@Ichoran
Copy link
Contributor
Ichoran commented F438 Nov 15, 2016

@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.
If there is a difference, I still wouldn't expect it to necessarily work out in favor of using Java for Scala (and vice-versa, I suppose): by removing the Scala code from a more normal context, you might make the microbenchmarking results even less relevant to normal usage than microbenchmarks usually are. For instance, if javac uses a pattern around the benchmark that is really easy for the JIT compiler to optimize away, but almost all use cases are from Scala with scalac's not-exactly-the-same pattern, you might fail to see an effect that is bothering everyone.
But really, the point is if it matters, it's already being done wrong. (The ops/s reported here are pretty well into the safe range where you could even call with interpreted Groovy and it'd be fine.)

@Ichoran
Copy link
Contributor
Ichoran commented Nov 15, 2016

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.

@retronym
Copy link
Member
retronym commented Nov 16, 2016

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

This is okay because the class already inherits a method with and identical signature from AbstractMap. Client code of 2.12.1 may include invokevirtual HashMap.getOrElseUpdate, but this will not incur an IncompatibleClassChangeError if linked against the 2.12.0 class thanks to the override.

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!)

@Ichoran
Copy link
Contributor
Ichoran commented Nov 16, 2016

@retronym - Okay, then I think we're clear once @paplorinc switches to use table and friends instead of addEntry0 etc.. Hopefully that will still show a speedup. The JIT compiler won't be able to take advantage of its efforts in already optimizing addEntry0, but hopefully it'll do just as well on the inlined version.

@l0rinc
Copy link
Contributor Author
l0rinc commented Nov 16, 2016

@Ichoran, to be sure I understand what you're requesting, do you want me to keep copy-pasting things from HashTable until everything compiles? That would result in duplicating even some fields:

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 HashTable to DRY it, if you think that's better than exposing two stateful metohods :)

edit3: I could annotate the two exposed methods with @migration("exposed for internal optimizations, might change in the future", "2.12.1")

@Ichoran
Copy link
Contributor
Ichoran commented Nov 16, 2016

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

@retronym
Copy link
Member

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

@l0rinc
Copy link
Contributor Author
l0rinc commented Nov 16, 2016

@Ichoran, thanks for the idea, I've tested it from the new Scala benchmarks and they maintained their speed advantage (except for the case when it actually needs growing, but that's ok), and the code is also still maintainable - great balance!

Please wait with the merge until I validate the results from Java also (I get some strange results that I need to investigate)

@retronym, thank you for your investigations!

@retronym
Copy link
Member

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.

e = e.next
e
}
private[this] def notFound(key: A, e: Entry): Boolean = (e != null) && !elemEquals(e.key, key)
Copy link
Contributor Author

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 = {
Copy link
Contributor Author

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 = {
Copy link
Contributor Author

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) = {
Copy link
Contributor Author

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

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!

Copy link
Contributor

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);
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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

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 :)

Copy link
Contributor

Choose a reason for hiding this comment

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

This looks reasonable, thanks :)


return xn.equals(y);
static boolean equalsNumObject(java.lang.Number xn, Object y) {
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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

Copy link
Contributor Author

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 :)

@l0rinc l0rinc force-pushed the getOrElseUpdate branch 3 times, most recently from 5cc7418 to b1c83ab Compare November 17, 2016 15:30
@@ -18,8 +16,7 @@ Using this example, one would simply run

jmh:runMain scala.collection.mutable.OpenHashMapRunner
Copy link
Contributor Author

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 = {
Copy link
Contributor Author
@l0rinc l0rinc Nov 17, 2016

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) {
Copy link
Contributor Author

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`
@l0rinc
Copy link
Contributor Author
l0rinc commented Nov 18, 2016

@Ichoran, not sure why the build is failing do I still have a MiMa violation?
library/mima failed: java.lang.RuntimeException: MiMa failed with exit code 1

@szeiger
Copy link
Contributor
szeiger commented Nov 18, 2016
[info] Checking backward binary compatibility
[info] prev = /home/jenkins/.ivy2/cache/org.scala-lang/scala-library/jars/scala-library-2.12.0.jar, curr = /home/jenkins/workspace/scala-2.12.x-validate-test@2/build/pack/lib/scala-library.jar
Found 1 binary incompatibilities
================================
 * method getExitValue()scala.Tuple2 in class
   scala.sys.process.ProcessImpl#CompoundProcess does not have a correspondent
   in current version

Generated backward filter config definition
============================================

    filter {
        problems=[
            {
                matchName="scala.sys.process.ProcessImpl#CompoundProcess.getExitValue"
                problemName=DirectMissingMethodProblem
            }
        ]
    }


Generated forward filter config definition
===========================================

    filter {
        problems=[]
    }

@szeiger
Copy link
Contributor
szeiger commented Nov 18, 2016

This looks like an unrelated failure due to MiMa having been accidentally disabled for a while.

@l0rinc
Copy link
Contributor Author
l0rinc commented Nov 18, 2016

@szeiger, what should I do to fix it (tried rebasing and retriggering the build)?

@szeiger
Copy link
Contributor
szeiger commented Nov 18, 2016

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

Copy link
Contributor
@Ichoran Ichoran left a comment

Choose a reason for hiding this comment

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

LGTM

@retronym
Copy link
Member

LGTM, too. This is a fantastic contribution, keep them coming!

@retronym retronym merged commit 9c5d3f8 into scala:2.12.x Nov 22, 2016
@l0rinc l0rinc deleted the getOrElseUpdate branch November 22, 2016 09:31
@l0rinc
Copy link
Contributor Author
l0rinc commented Nov 22, 2016

Thanks for your help @Ichoran, @retronym and @szeiger!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants
0