title: Missing bounds checking elimination in String constructor?
body: <p>Looking into UTF8 decoding performance, I noticed the performance of protobuf's <code>UnsafeProcessor::decodeUtf8</code> is better than <code>String(byte[] bytes, int offset, int length, Charset charset)</code> for the following non ascii string: <code>&quot;Quizdeltagerne spiste jordbær med flØde, mens cirkusklovnen&quot;</code>.</p>
  <p>I tried to figure out why, so I copied the relevant code in <code>String</code> and replaced the array accesses with unsafe array accesses, same as <code>UnsafeProcessor::decodeUtf8</code>.
  Here are the JMH benchmark results:</p>
  <pre><code>Benchmark                       Mode  Cnt    Score   Error  Units
  StringBenchmark.safeDecoding    avgt   10  127.107 ± 3.642  ns/op
  StringBenchmark.unsafeDecoding  avgt   10  100.915 ± 4.090  ns/op
  </code></pre>
  <p>I assume the difference is due to missing bounds checking elimination which I expected to kick in, especially since there is an explicit bounds check in the form of a call to <code>checkBoundsOffCount(offset, length, bytes.length)</code> in the beginning of <code>String(byte[] bytes, int offset, int length, Charset charset)</code>.</p>
  <p>Is the issue really a missing bounds check elimination?</p>
  <p>Here's the code I benchmarked using OpenJDK 17 &amp; JMH. Note that this is only part of the <code>String(byte[] bytes, int offset, int length, Charset charset)</code> constructor code, and works correctly only for this specific German String.
  The static methods were copied from <code>String</code>.
  Look for the <code>// the unsafe version:</code> comments that indicate where I replaced the safe access with unsafe.</p>
  <pre><code>    private static byte[] safeDecode(byte[] bytes, int offset, int length) {
          checkBoundsOffCount(offset, length, bytes.length);
          int sl = offset + length;
          int dp = 0;
          byte[] dst = new byte[length];
          while (offset &lt; sl) {
              int b1 = bytes[offset];
              // the unsafe version:
              // int b1 = UnsafeUtil.getByte(bytes, offset);
              if (b1 &gt;= 0) {
                  dst[dp++] = (byte)b1;
                  offset++;
                  continue;
              }
              if ((b1 == (byte)0xc2 || b1 == (byte)0xc3) &amp;&amp;
                      offset + 1 &lt; sl) {
                  // the unsafe version:
                  // int b2 = UnsafeUtil.getByte(bytes, offset + 1);
                  int b2 = bytes[offset + 1];
                  if (!isNotContinuation(b2)) {
                      dst[dp++] = (byte)decode2(b1, b2);
                      offset += 2;
                      continue;
                  }
              }
              // anything not a latin1, including the repl
              // we have to go with the utf16
              break;
          }
          if (offset == sl) {
              if (dp != dst.length) {
                  dst = Arrays.copyOf(dst, dp);
              }
              return dst;
          }
  
          return dst;
      }
  </code></pre>
  <h2>Followup</h2>
  <p>Apparently if I change the while loop condition from <code>offset &lt; sl</code> to <code>0 &lt;= offset &amp;&amp; offset &lt; sl</code>
  I get similar performance in both versions:</p>
  <pre><code>Benchmark                       Mode  Cnt    Score    Error  Units
  StringBenchmark.safeDecoding    avgt   10  100.802 ± 13.147  ns/op
  StringBenchmark.unsafeDecoding  avgt   10  102.774 ± 3.893  ns/op
  </code></pre>
  <h2>Conclusion</h2>
  <p>This question was picked up by HotSpot developers as <a href="https://bugs.openjdk.java.net/browse/JDK-8278518" rel="nofollow noreferrer">https://bugs.openjdk.java.net/browse/JDK-8278518</a>.</p>
  <p>Optimizing this code ended up giving a 2.5x boost to decoding the above Latin1 string.</p>
  <p>This C2 optimization closes the unbelievable more than <strong>7x</strong> gap between <code>commonBranchFirst</code> and <code>commonBranchSecond</code> in the below benchmark and will land in Java 19.</p>
  <pre><code>Benchmark                         Mode  Cnt     Score    Error  Units
  LoopBenchmark.commonBranchFirst   avgt   25  1737.111 ± 56.526  ns/op
  LoopBenchmark.commonBranchSecond  avgt   25   232.798 ± 12.676  ns/op
  </code></pre>
  <pre><code>@State(Scope.Thread)
  @BenchmarkMode(Mode.AverageTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public class LoopBenchmark {
  
    private final boolean[] mostlyTrue = new boolean[1000];
  
    @Setup
    public void setup() {
      for (int i = 0; i &lt; mostlyTrue.length; i++) {
        mostlyTrue[i] = i % 100 &gt; 0;
      }
    }
  
    @Benchmark
    public int commonBranchFirst() {
      int i = 0;
      while (i &lt; mostlyTrue.length) {
        if (mostlyTrue[i]) {
          i++;
        } else {
          i += 2;
        }
      }
      return i;
    }
  
    @Benchmark
    public int commonBranchSecond() {
      int i = 0;
      while (i &lt; mostlyTrue.length) {
        if (!mostlyTrue[i]) {
          i += 2;
        } else {
          i++;
        }
      }
      return i;
    }
  }
  </code></pre>