JDK-8279833 : Loop optimization issue in String.encodeUTF8_UTF16
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.lang
  • Affected Version: 9,11,17,18,19
  • Priority: P2
  • Status: Resolved
  • Resolution: Fixed
  • Submitted: 2022-01-11
  • Updated: 2022-01-25
  • Resolved: 2022-01-11
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11 JDK 17 JDK 18 JDK 19
11.0.15-oracleFixed 17.0.3-oracleFixed 18Fixed 19 b05Fixed
Related Reports
Relates :  
Description
While making an attempt to replace the ASCII fast loop in `String.encodeUTF8_UTF16` I noticed that altering the shape of the code so that char c is scope local to each loop helps the performance of the method by helping C2 optimize each loop better. I narrowed it down to something as straightforward as this:

```
diff --git a/src/java.base/share/classes/java/lang/String.java b/src/java.base/share/classes/java/lang/String.java
index abb35ebaeb1..f84d60f92cc 100644
--- a/src/java.base/share/classes/java/lang/String.java
+++ b/src/java.base/share/classes/java/lang/String.java
@@ -1284,14 +1284,17 @@ public final class String
         int sp = 0;
         int sl = val.length >> 1;
         byte[] dst = new byte[sl * 3];
-        char c;
-        while (sp < sl && (c = StringUTF16.getChar(val, sp)) < '\u0080') {
+        while (sp < sl) {
+            char c = StringUTF16.getChar(val, sp);
+            if (c >= '\u0080') {
+                break;
+            }
             // ascii fast loop;
             dst[dp++] = (byte)c;
             sp++;
         }
         while (sp < sl) {
-            c = StringUTF16.getChar(val, sp++);
+            char c = StringUTF16.getChar(val, sp++);
             if (c < 0x80) {
                 dst[dp++] = (byte)c;
             } else if (c < 0x800) {
```

Results on a few micros I'm updating to better stress this code --
Baseline:
```
Benchmark                                      (charsetName)  Mode  Cnt     Score     Error  Units
StringEncode.WithCharset.encodeUTF16                   UTF-8  avgt   15   171.853 ±  10.275  ns/op
StringEncode.WithCharset.encodeUTF16LongEnd            UTF-8  avgt   15  1991.586 ±  82.234  ns/op
StringEncode.WithCharset.encodeUTF16LongStart          UTF-8  avgt   15  8422.458 ± 473.161  ns/op
```
Patch:
```
Benchmark                                      (charsetName)  Mode  Cnt     Score     Error  Units
StringEncode.WithCharset.encodeUTF16                   UTF-8  avgt   15   128.525 ±   6.573  ns/op
StringEncode.WithCharset.encodeUTF16LongEnd            UTF-8  avgt   15  1843.455 ±  72.984  ns/op
StringEncode.WithCharset.encodeUTF16LongStart          UTF-8  avgt   15  4124.791 ± 308.683  ns/op
```

Going back, this seem to have been an issue with this code since its inception with JEP 254 in JDK 9.

The micro encodeUTF16LongEnd encodes a longer string which is mostly ASCII but with an non-ASCII codepoint at the end. This exaggerates the usefulness of the ascii loop. encodeUTF16LongStart tests the same string but with the non-ASCII codepoint moved to the front. This stresses the non-ascii loop. We see that the patch above helps in general, but mainly improves the microbenchmark that spends its time in the second loop.

There's likely a compiler bug hiding in plain sight here where the potentially uninitialized local `char c` messes up the loop optimization of the second loop. I think the above patch is reasonable to put back into the JDK while we investigate if/how C2 can better handle this pattern.
Comments
A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk11u-dev/pull/791 Date: 2022-01-24 14:34:30 +0000
24-01-2022

Fix request [11u] I backport this for parity with 11.0.15-oracle. A simple fix that should do no harm. Backport is not clean, see PR. SAP nightly testing passed.
24-01-2022

Changeset: ff856593 Author: Claes Redestad <redestad@openjdk.org> Date: 2022-01-13 15:25:16 +0000 URL: https://git.openjdk.java.net/jdk/commit/ff8565931115d581afff679ea85b1a2d80c03b99
18-01-2022

> This is only a performance enhancement. That's a bit of a mis-characterization, though, since this fixes a sizeable regression introduced in JDK 9. I reported on those findings in the mainline PR: https://github.com/openjdk/jdk/pull/7026
17-01-2022

Fix request [17u] I backport this for parity with 17.0.3-oracle. This is only a performance enhancement. But the risk is low, the code change is very trivial. Thus I think we should take this. Clean backport. SAP nightly testing passed.
15-01-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk17u-dev/pull/97 Date: 2022-01-14 11:16:02 +0000
14-01-2022

Fix Request (18/18u) This resolves a regression to String.getBytes(UTF-8) for UTF-16 encoded strings introduced in JDK 9. Patch applies cleanly. (EDIT: this fix request wasn't necessary since we're still in RDP1 and the bug is P2)
13-01-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk18/pull/99 Date: 2022-01-13 13:46:53 +0000
13-01-2022

[~stsypanov]: there's some interesting interaction pointed out by [~roland] over in JDK-8279888 (scoping the locals differently seem to prohibit some speculative optimizations) but digging deeper the regression detected in this case seem unrelated. Let's move the conversation there.
12-01-2022

Is this related to https://bugs.openjdk.java.net/browse/JDK-8278518? What do you think?
12-01-2022

Changeset: c3d0a940 Author: Claes Redestad <redestad@openjdk.org> Date: 2022-01-11 14:49:03 +0000 URL: https://git.openjdk.java.net/jdk/commit/c3d0a94040d9bd0f4b99da97b89fbfce252a41c0
11-01-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk/pull/7026 Date: 2022-01-11 12:30:44 +0000
11-01-2022