8000 SI-6370 changed ListMap apply method to produce correct error message by ViniciusMiana · Pull Request #2009 · scala/scala · GitHub
[go: up one dir, main page]

Skip to content

SI-6370 changed ListMap apply method to produce correct error message #2009

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

Closed
wants to merge 1 commit into from
Closed

SI-6370 changed ListMap apply method to produce correct error message #2009

wants to merge 1 commit into from

Conversation

ViniciusMiana
Copy link
Contributor

Current implementation of apply uses tail recursive apply0 to get a value from a key. apply0 relies on tail method to iterate over all keys.When the list gets to its end, tail produces an 'empty map' message in its exception, which is thrown by ListMap. This change catches this exception in the apply method and change the exception message to a more appropriate 'key not found' message

review by @phaller

… when a key is not found

Current implementation of apply uses tail recursive apply0 to get a value from a key. apply0 relies on tail method to iterate over all keys.When the list gets to its end, tail produces an 'empty map' message in its exception, which is thrown by ListMap. This change catches this exception in the apply method and change the exception message to a more appropriate 'key not found' message

Signed-off-by: Vinicius Miana <vinicius@miana.com.br>
@adriaanm
Copy link
Contributor
adriaanm commented Feb 5, 2013

I'm not sure wrapping every apply call in a try/catch is the right way to go here.
Can't think of a better solution, though.

ping @phaller / @axel22 for review

@axel22
Copy link
Contributor
axel22 commented Feb 5, 2013

One solution would be to override apply in ListMap to throw the desired exception message - "key not found", but then we couldn't make apply0 tail recursive any more.

A better solution would be to modify the body of apply0 like this:

@tailrec private def apply0(cur: ListMap[A, B1], k: A): B1 =
  if (cur.isEmpty) throw new NoSuchElementException("no such key")
  else if (k == cur.key) cur.value
  else apply0(cur.tail, k)

Might be good to write a couple of microbenchmarks to verify how this affects traversal performance.

@ViniciusMiana
Copy link
Contributor Author

That was my first solution, but I did not want to add this extra if in the recursive function.
I think this one is more elegant, but I did not feel that bad by the try catch wrapper. Another thing that I thought would be to write a private tail0 that throws "no such key" and use that instead, but I thought that would be even uglier than the try catch wrapper. Do you want me to run a few benchmarks to see how much this extra 'if' would impact?

@axel22
Copy link
Contributor
axel22 commented Feb 5, 2013

You can.
I also see no problem with another private tail0.

@ViniciusMiana
Copy link
Contributor Author

The problem with private tail0 is that it would not have cur.key available for a nice error message, unless we pass that just for that reason. I would lean towards the apply0 if you feel that the try {} catch is not acceptable and the loss is performance is negligible. It is the solution with the cleaner code.

@axel22
Copy link
Contributor
axel22 commented Feb 5, 2013

If you intend to add apply0 into the ListMap base class and recursively call apply0 on the receiver, then it seems to me that the method can no longer be made tail-recursive. That wouldn't be acceptable.
Unless you had something else in mind?

@ViniciusMiana
Copy link
Contributor Author

Actually did not went that far, as I did not like the idea to pass the current key for a nice message.
I think we are down to 2 options:

  1. My commit - the try catch wrap
  2. The extra if in the apply0 that you commented.

@axel22
Copy link
Contributor
axel22 commented Feb 5, 2013

Ok, in that case, we should justify the costs of the extra if. Since ListMap is not an interface, that one should amount to an invokevirtual, so I'm guessing it should still have similar performance. But we should have a microbenchmark.

@ViniciusMiana
Copy link
Contributor Author

There is no statistically meaningful difference between the try-catch and the if in terms of time to do up to 100.000 calls to apply.
Looking into the bytecode, here is how it looks:
Original implementation:
private java.lang.Object apply0(scala.collection.immutable.ListMap, java.lang.Object);
Code:
0: aload_2
1: aload_1
2: invokevirtual #74; //Method scala/collection/immutable/ListMap.key:()Ljava/lang/Object;
5: invokestatic #80; //Method scala/runtime/BoxesRunTime.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z
8: ifeq 16
11: aload_1
12: invokevirtual #82; //Method scala/collection/immutable/ListMap.value:()Ljava/lang/Object;
15: areturn
16: aload_1
17: invokevirtual #40; //Method scala/collection/immutable/ListMap.tail:()Lscala/collection/immutable/ListMap;
20: astore_1
21: goto 0
If-else proposed by @axel22 implementation:
private java.lang.Object apply(scala.collection.immutable.ListMap, java.lang.Object);
Code:
0: aload_1
1: invokeinterface #36, 1; //InterfaceMethod scala/collection/MapLike.isEmpty:()Z
6: ifeq 19
9: new #53; //class java/util/NoSuchElementException
12: dup
13: ldc #85; //String no such key
15: invokespecial #72; //Method java/util/NoSuchElementException."":(Ljava/lang/String;)V
18: athrow
19: aload_2
20: aload_1
21: invokevirtual #74; //Method scala/collection/immutable/ListMap.key:()Ljava/lang/Object;
24: invokestatic #80; //Method scala/runtime/BoxesRunTime.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z
27: ifeq 35
30: aload_1
31: invokevirtual #82; //Method scala/collection/immutable/ListMap.value:()Ljava/lang/Object;
34: areturn
35: aload_1
36: invokevirtual #40; //Method scala/collection/immutable/ListMap.tail:()Lscala/collection/immutable/ListMap;
39: astore_1
40: goto 0

@ViniciusMiana
Copy link
Contributor Author

And here the two versions of the apply without and with the try catch wrapper:
public java.lang.Object apply(java.lang.Object);
Code:
0: aload_0
1: aload_0
2: aload_1
3: invokespecial #49; //Method apply0:(Lscala/collection/immutable/ListMap;Ljava/lang/Object;)Ljava/lang/Object;
6: areturn
Version with the try catch wrapper (current commit in the PR)
public java.lang.Object apply(java.lang.Object);
Code:
0: aload_0
1: aload_0
2: aload_1
3: invokespecial #49; //Method apply0:(Lscala/collection/immutable/ListMap;Ljava/lang/Object;)Ljava/lang/Object;
6: areturn
7: astore_2
8: new #53; //class java/util/NoSuchElementException
11: dup
12: new #55; //class scala/collection/mutable/StringBuilder
15: dup
16: invokespecial #59; //Method scala/collection/mutable/StringBuilder."":()V
19: ldc #61; //String key not found:
21: invokevirtual #65; //Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;
24: aload_1
25: invokevirtual #65; //Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;
28: invokevirtual #69; //Method scala/collection/mutable/StringBuilder.toString:()Ljava/lang/String;
31: invokespecial #72; //Method java/util/NoSuchElementException."":(Ljava/lang/String;)V
34: athrow
Exception table:
from to target type
0 6 7 Class java/util/NoSuchElementException

@ViniciusMiana
Copy link
Contributor Author

Conclusion: the difference is performance is not significant, the extra if adds just one instruction in the recursive call and in the end it does not make any difference. I will amend the commit and change it to the recursive if, ok?

@axel22
Copy link
Contributor
axel22 commented Feb 5, 2013

I really appreciate you taking the time to inspect the bytecode. However, since there are only a few people on the globe who know so much about the JIT and the JVM to accurately predict the performance effect of adding instructions in particular locations in the bytecode, the conclusion that adding a single instruction makes no difference is at best shady.
Besides, my proposed version includes an invokeinterface at the beginning, which is reportedly a much more expensive operation than an invokevirtual.

I therefore warmly recommend a microbenchmark to verify these assumptions, in particular 2 of them:

  1. evaluating the running time of an apply call on a list map with many elements
  2. evaluating the running time of many apply calls on a list map with a few elements (seems to be a more typical use case)

There are some docs on writing precise microbenchmark, see here for example: http://docs.scala-lang.org/overviews/parallel-collections/performance.html

Or did I misunderstand and you already ran a benchmark? Could you link to a gist?

Thanks

@ViniciusMiana
Copy link
Contributor Author

I would say that inspect that byte-code is not only shady, but naive. I put up the bytecode, thinking that you could be one of those people or someone else on this list. I already did a benchmark:
"There is no statistically meaningful difference between the try-catch and the if in terms of time to do up to 100.000 calls to apply. "
I reviewed the micro benchmark doc and there were a few things that I did not do, like use java server to run it, so I will redo it, see if the difference is still not meaningful and post the results and my conclusion.

@axel22
Copy link
Contributor
axel22 commented Feb 6, 2013

You honour me greatly, but I'm not one of those :)

Ok, looking forward to see the results. Thanks a lot!

@ViniciusMiana
Copy link
Contributor Author

I changed the tests following the instructions you posted, but I did not get anything statiscally meaningful. You can see 2 consecutive runs of the original ListMap for big sizes:
ListMapBench$ 27531 28020 28205 28148 26727 22096 21013 21145 20912 20870
ListMapBench$ 27772 28653 28336 28466 26698 22588 21850 21813 22053 22134
In one it did better than both of our fixes and on the other it did not. You can see more on: https://gist.github.com/ViniciusMiana/4725280
I can go either way, keep this commit and we have a try-catch wrapper or amend to a if in apply0. My intuition says the try-catch performs better and I find the if code a more elegant way to do it.
What do you think?
Is @phaller going to give his input to?

@phaller
Copy link
Contributor
phaller commented Feb 7, 2013

I just had a look at your benchmarking results. It appears to me that for small sizes, the apply0/if option seems a bit faster. This is important, since some high-throughput applications create many, many small collections (the Scala compiler is one example; I'm not saying ListMap is used pervasively in the compiler, though!). So, since you're OK going either way, I would suggest going the if-in-apply0 route that Alex suggested.

Thanks.

@axel22
Copy link
Contributor
axel22 commented Feb 7, 2013

Yep, the extra if version seems to work slightly better in the microbenchmark.
I think we should stick to that version.

Thanks a lot!

@adriaanm
Copy link
Contributor
adriaanm commented Feb 7, 2013

I would also prefer the solution without the try/catch. Please open a new PR once the 2.10.x branch becomes the target for fixes going into 2.10.2 (I'll probably branch for 2.10.1 on Friday Feb 8). I think we're too close to RC1 to change this now anyway -- sorry.

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.

4 participants
0