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
[perf] Integer#chr(Encoding::UTF_8) is considerably slower than CRuby #2316
Comments
I would be willing to bet the issue is the raise of RangeError. As I noted in jruby/jruby#6652 (comment), your JRuby can eliminate the backtrace but only in the case of a "simple" rescue that does not do side-effecty things like looking up constants. If you were to do a bare |
A modified version of your bench that only does the direct raise, with and without the require 'benchmark/ips'
UNPRINTABLE = ??
class Integer
def bad_chr
raise(RangeError,"test")
end
end
Benchmark.ips do |b|
b.report("RangeError with const lookup") do
begin
1.bad_chr
rescue RangeError
UNPRINTABLE
end
end
b.report("RangeError no const lookup") do
begin
1.bad_chr
rescue RangeError
end
end
end
|
This second case is really a no-op: begin
raise RangeError
rescue RangeError
end We can see at compile-time that the class of the exception is the same as the class in the goto foo
foo: Then the Your code has a method between the The second case should be the same thing, right?
But I'm afraid we actually manually not inline the key primitive at the end of the chain at the moment: truffleruby/src/main/java/org/truffleruby/core/string/StringNodes.java Lines 3980 to 4024 in d37fbf7
This is because we considered it for some reason to be a 'slow path' more unusual or We could probably fix that if you think this should be a fast operation? Possible we could just move the exception throwing outside the non-inlined method, even if the rest of the logic isn't inlined. We may find after fixing that primitive to make sure it is inlined that we find something else isn't inlined that also interferes with turning the exception into a |
@chrisseaton But the RangeError constant might be removed in another thread. How does your optimization handle that case, when the rescue exception lookup would call into a |
If another thread wants to redefine a constant which this machine code uses, such as As there are no side effects between the raise and rescue, and no significant computation either, no opportunity is provided for that interruption to happen between them and it is not possible to have created the exception and not yet have rescued it - the raise and rescue become atomic and any redefinition must happen either before or after the rescue. If another thread repeatedly redefines the constant, we will stop applying this optimisation for this constant so that we do not keep deoptimising. |
@chrisseaton This would be a bug (or at least behavior inconsistent with CRuby), would it not? Were this constant being checked every time, the update would be visible between the raise and the rescue, and the backtrace would need to be in hand before proceeding to the constant lookup. You are optimizing it assuming that the exception constant can't be removed between the raise and the rescue, but that could happen on CRuby. |
No, absolutely not. What happens is that the thread which wants to redefine the constant has to wait before it is allowed to redefine the constant. Conceptually, it isn't allowed to redefine when the other thread is in between this
How would you trigger the update so that it must appear between the |
@postmodern I think I fixed it by moving the exception raise outside the non-inlined code. Is this what you wanted to see? The baseline is slower because I'm running a development build on the JVM, but the point is I reduced the exception cost.
diff --git a/src/main/java/org/truffleruby/core/exception/CoreExceptions.java b/src/main/java/org/truffleruby/core/exception/CoreExceptions.java
index 4312c5f340..30060eade3 100644
--- a/src/main/java/org/truffleruby/core/exception/CoreExceptions.java
+++ b/src/main/java/org/truffleruby/core/exception/CoreExceptions.java
@@ -1034,10 +1034,15 @@ public class CoreExceptions {
@TruffleBoundary
public RubyException rangeError(long code, RubyEncoding encoding, Node currentNode) {
return rangeError(
- StringUtils.format("invalid codepoint %x in %s", code, encoding.encoding),
+ rangeErrorMessage(code, encoding.encoding),
currentNode);
}
+ @TruffleBoundary
+ private String rangeErrorMessage(long code, Encoding encoding) {
+ return StringUtils.format("invalid codepoint %x in %s", code, encoding);
+ }
+
@TruffleBoundary
public RubyException rangeError(RubyIntRange range, Node currentNode) {
return rangeError(StringUtils.format(
diff --git a/src/main/java/org/truffleruby/core/string/StringNodes.java b/src/main/java/org/truffleruby/core/string/StringNodes.java
index 907c13b9fd..6cf699daea 100644
--- a/src/main/java/org/truffleruby/core/string/StringNodes.java
+++ b/src/main/java/org/truffleruby/core/string/StringNodes.java
@@ -3977,26 +3977,18 @@ public abstract class StringNodes {
return makeStringNode.fromRope(rope);
}
- @TruffleBoundary
@Specialization(guards = { "!isSimple(code, rubyEncoding)", "isCodepoint(code)" })
protected RubyString stringFromCodepoint(long code, RubyEncoding rubyEncoding,
@Cached RopeNodes.CalculateCharacterLengthNode calculateCharacterLengthNode) {
final Encoding encoding = rubyEncoding.encoding;
- final int length;
-
- try {
- length = encoding.codeToMbcLength((int) code);
- } catch (EncodingException e) {
- throw new RaiseException(getContext(), coreExceptions().rangeError(code, rubyEncoding, this));
- }
+ final int length = codeToMbcLength(encoding, (int) code);
if (length <= 0) {
throw new RaiseException(getContext(), coreExceptions().rangeError(code, rubyEncoding, this));
}
final byte[] bytes = new byte[length];
-
- final int codeToMbc = encoding.codeToMbc((int) code, bytes, 0);
+ final int codeToMbc = codeToMbc(encoding, (int) code, bytes, 0);
if (codeToMbc < 0) {
throw new RaiseException(getContext(), coreExceptions().rangeError(code, rubyEncoding, this));
}
@@ -4009,6 +4001,20 @@ public abstract class StringNodes {
return makeStringNode.executeMake(bytes, encoding, CodeRange.CR_VALID);
}
+ @TruffleBoundary
+ private int codeToMbcLength(Encoding encoding, int code) {
+ try {
+ return encoding.codeToMbcLength(code);
+ } catch (EncodingException e) {
+ return -1;
+ }
+ }
+
+ @TruffleBoundary
+ private int codeToMbc(Encoding encoding, int code, byte[] bytes, int p) {
+ return encoding.codeToMbc(code, bytes, p);
+ }
+
protected boolean isCodepoint(long code) {
// Fits in an unsigned int
return code >= 0 && code < (1L << 32); |
@chrisseaton That looks good, could you make a PR with that? :) |
I will preface this by saying that I do not believe the update necessarily should be visible between the raise and the rescue, but a strict interpretation of rescue behavior implies that this is so.
The behavior of rescue with a constant is that it performs that constant lookup at the time of the rescue and invokes You are making the window between an inlined raise and rescue into a critical section that prevents another thread from performing its constant update until after the window has closed. That is arguably an OK optimization but it does not fit a strict interpretation of rescue semantics in the presence of threads. |
@postmodern this is the same configuration as you're running
So I do fundamentally agree with @headius on the point the ultimately it'd be better not to optimise away the exception and to use a return code instead for fast-path operations. |
I would argue - how do you tell the difference between an update which by chance never happens to be observed between them but could be, and one that will never be observed between them?
Yes - exactly the same as when |
@chrisseaton A simpler way to say what I am saying:
Of course, that behavior change might be fine. Your example of I think part of my issue here is that CRuby can context switch potentially on any method call boundary, and potentially also on other cache boundaries like constants (I have not checked in a while). By sweeping away those boundaries you avoid unnecessary guards, but also change behavior. |
I can agree that you may be able to detect a statistical difference in the locations where interrupts are observed when optimisations like this are applied. We do have this disclaimer in our compatibility claims. In the past (I think Sidekiq?) we actually had a test failure because we were detecting them in more places than MRI. I've never see a test failure due to the other way around. |
@chrisseaton We have had issues filed in the past about our interrupt checks being too coarse-grained, but I think they were all in a long-lost bug tracker before we moved to Github. I don't recall that they were real-world examples... more like someone attempting to prove a weird concurrent edge case differed from CRuby. This was during a time when we were the only parallel-executing Ruby and were still figuring out how fine-grained our interrupts needed to be. In any case this is mostly academic since there's no predictable way to make the constant update in one thread and know another thread will see it mid-rescue without introducing other side-effecty code. We will expand JRuby's backtrace elimination logic to cover explicit rescues like this case. @postmodern ...but we really should avoid the exception anyway since one false move and it will be back with a vengeance. |
Yes, in the long-run it would be nice if there was a way to directly query an |
Fixed by #2318, the performance is then similar to CRuby for |
I discovered this performance issue when debugging an issue with calling
Integer#chr
on invalid multi-bytes UTF-8 words taking considerably longer on TruffleRuby than CRuby. The issue does not seem to be related to raising/rescuing the RangeError exception.Example
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
truffleruby 21.0.0, like ruby 2.7.2, GraalVM CE Native [x86_64-linux]
The text was updated successfully, but these errors were encountered: