Как ухудшить производительность, улучшая её

habr.png

Хотели как лучше, а получилось как всегда.
Виктор Черномырдин,
русский государственный деятель

Бывают в жизни случаи, когда ты вроде бы всё делаешь правильно, но что-то идёт не так.
Этот рассказ об одном из таких случаев.

Однажды я смотрел на этот код и думал о его ускорении:

public String appendBounds(Data data) {
  int beginIndex = data.beginIndex;
  int endIndex = data.endIndex;

  return new StringBuilder()
          .append('L')
          .append(data.str, beginIndex, endIndex)
          .append(';')
          .toString();
}

Сперва я хотел вычислять итоговую длину строки, использую переменные beginIndex и endIndex(а также тот факт, что кроме урезанной строки к StringBuilder-у будет добавлено ещё 2 знака), и передавать это значение в конструктор StringBuilder-а, чтобы сразу выделить массив нужного размера. Эта мысль показалась мне чересчур очевидной, поэтому я решил испробовать что-то ещё. К верной мысли меня подтолкнуло то обстоятельство, что данный код никак не подсвечивался «Идеей», хотя эта умница обычно предлагает заменить короткую цепочку из StringBuilder::append на сложение строк, что короче и легче воспринимается.

Помехой для подобного упрощения является использование метода StringBuilder.append(CharSequence, int, int). Учитывая, что поле data.str является строкой, с помощью String.substring(beginIndex, endIndex) можно выделить из неё подстроку и передать методу StringBuilder.append(String).

Код после преобразования:

public String appendBounds(Data data) {
  int beginIndex = data.beginIndex;
  int endIndex = data.endIndex;

  String subString = data.str.substring(beginIndex, endIndex);

  return new StringBuilder()
          .append('L')
          .append(subString)
          .append(';')
          .toString();
}

И вот теперь «Идея» предлагает упрощение:

public String appendBounds(Data data) {
  int beginIndex = data.beginIndex;
  int endIndex = data.endIndex;

  return 'L' + data.str.substring(beginIndex, endIndex) + ';';
}

Однако, нашей целью в данном случае является не столько читаемость, сколько производительность. Сравним оба способа:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
public class StringBuilderAppendBenchmark {

  @Benchmark
  public String appendSubString(Data data) {
    String latinStr = data.latinStr;
    String nonLatinStr = data.nonLatinStr;
    int beginIndex = data.beginIndex;
    int endIndex = data.endIndex;

    String substring = data.nonLatin ?
            nonLatinStr.substring(beginIndex, endIndex) :
            latinStr.substring(beginIndex, endIndex);

    return new StringBuilder()
            .append('L')
            .append(substring)
            .append(';')
            .toString();
  }

  @Benchmark
  public String appendBounds(Data data) {
    String latinStr = data.latinStr;
    String nonLatinStr = data.nonLatinStr;
    int beginIndex = data.beginIndex;
    int endIndex = data.endIndex;

    String appended = data.nonLatin ? nonLatinStr : latinStr;

    return new StringBuilder()
            .append('L')
            .append(appended, beginIndex, endIndex)
            .append(';')
            .toString();
  }

  @State(Scope.Thread)
  public static class Data {
    String latinStr;
    String nonLatinStr;

    @Param({"true", "false"})
    boolean nonLatin;

    @Param({"5", "10", "50", "100", "500", "1000"})
    private int length;

    private int beginIndex;
    private int endIndex;

    private ThreadLocalRandom random = ThreadLocalRandom.current();

    @Setup
    public void setup() {
      latinStr = randomString("abcdefghijklmnopqrstuvwxyz");
      nonLatinStr = randomString("абвгдеёжзиклмнопрстуфхцчшщьыъэюя");
      beginIndex = 1;
      endIndex = length + 1;
    }

    private String randomString(String alphabet) {
      char[] chars = alphabet.toCharArray();

      StringBuilder sb = new StringBuilder(length + 2);
      for (int i = 0; i < length + 2; i++) {
        char c = chars[random.nextInt(chars.length)];
        sb.append(c);
      }

      return sb.toString();
    }
  }
}

Бенчмарк прост как две копейки: к StringBuilder-у добавляется случайная строка, размер которой определяется полем length, а поскольку на дворе 2019 год, то проверять нужно как строку, содержащую только знаки основной латиницы (т. н. сжатая строка, в которой каждому знаку соответствует 1 байт), так и строку с нелатинскими знаками (каждый символ представлен 2 байтами).

При поверхностном рассмотрении метод appendSubString представляется нам более медленным, ибо объём приклеиваемых данных совпадает с таковым в методе appendBounds, однако в методе appendSubString есть ещё и явное создание подстроки, т. е. выделение памяти на новый объект и копирование в него содержимого из data.latinStr / data.nonLatinStr.

Тем более удивительными (но только на первый взгляд) кажутся итоги измерения, выполненного мной с помощью JDK11 на домашней машине (Intel Core i5–4690, 3.50 ГГц):

Benchmark                           nonLatin   length     Score   Error   Units
appendBounds                            true        5      44,6 ±   0,4   ns/op
appendBounds                            true       10      45,7 ±   0,7   ns/op
appendBounds                            true       50     129,0 ±   0,5   ns/op
appendBounds                            true      100     218,7 ±   0,8   ns/op
appendBounds                            true      500     907,1 ±   5,5   ns/op
appendBounds                            true     1000    1626,4 ±  13,0   ns/op

appendSubString                         true        5      28,6 ±   0,2   ns/op
appendSubString                         true       10      30,8 ±   0,2   ns/op
appendSubString                         true       50      65,6 ±   0,4   ns/op
appendSubString                         true      100     106,6 ±   0,6   ns/op
appendSubString                         true      500     430,1 ±   2,4   ns/op
appendSubString                         true     1000     839,1 ±   8,6   ns/op

appendBounds:·gc.alloc.rate.norm        true        5     184,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm        true       10     200,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm        true       50     688,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm        true      100    1192,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm        true      500    5192,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm        true     1000   10200,0 ±   0,0    B/op

appendSubString:·gc.alloc.rate.norm     true        5     136,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm     true       10     160,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm     true       50     360,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm     true      100     608,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm     true      500    2608,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm     true     1000    5104,0 ±   0,0    B/op

appendBounds                           false        5      20,8 ±   0,1   ns/op
appendBounds                           false       10      24,0 ±   0,2   ns/op
appendBounds                           false       50      66,4 ±   0,4   ns/op
appendBounds                           false      100     111,0 ±   0,8   ns/op
appendBounds                           false      500     419,2 ±   2,7   ns/op
appendBounds                           false     1000     840,4 ±   7,8   ns/op

appendSubString                        false        5      25,3 ±   0,3   ns/op
appendSubString                        false       10      25,7 ±   0,2   ns/op
appendSubString                        false       50      36,0 ±   0,1   ns/op
appendSubString                        false      100      52,8 ±   0,4   ns/op
appendSubString                        false      500     206,1 ±   6,1   ns/op
appendSubString                        false     1000     388,1 ±   1,6   ns/op

appendBounds:·gc.alloc.rate.norm       false        5      80,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm       false       10      88,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm       false       50     320,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm       false      100     544,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm       false      500    2144,0 ±   0,0    B/op
appendBounds:·gc.alloc.rate.norm       false     1000    4152,0 ±   0,0    B/op

appendSubString:·gc.alloc.rate.norm    false        5      96,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm    false       10     112,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm    false       50     192,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm    false      100     288,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm    false      500    1088,0 ±   0,0    B/op
appendSubString:·gc.alloc.rate.norm    false     1000    2088,0 ±   0,0    B/op

Опровергая наше предположение метод appendSubString в подавляющем большинстве случаев (в т. ч. всегда для нелатинских строк) оказался и более быстрым, и менее прожорливым (и это при том, что String::substring возвращает новый объект). Как же так получилось?


Смотрю в книгу, вижу фигу

Приподнять завесу тайны поможет изучение исходного кода StringBuilder-а. Оба используемых метода передают вызов в такие же методы AbstractStringBuilder-а:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable, CharSequence {

  @Override
  public StringBuilder append(String str) {
    super.append(str);
    return this;
  }

  @Override
  public StringBuilder append(CharSequence s, int start, int end) {
    super.append(s, start, end);
    return this;
  }
}

Переходим к AbstractStringBuilder.append(String):

public AbstractStringBuilder append(String str) {
  if (str == null) {
    return appendNull();
  }
  int len = str.length();
  ensureCapacityInternal(count + len);
  putStringAt(count, str);
  count += len;
  return this;
}

private final void putStringAt(int index, String str) {
  if (getCoder() != str.coder()) {
    inflate();
  }
  str.getBytes(value, index, coder);
}

Что здесь интересно? Метод AbstractStringBuilder::inflate, как следует из названия, расширяет массив AbstractStringBuilder.value при объединении разнородных строк. Перемещение же данных происходит в методе String::getBytes:

void getBytes(byte[] dst, int dstBegin, byte coder) {
  if (coder() == coder) {
    System.arraycopy(value, 0, dst, dstBegin << coder, value.length);
  } else {    // this.coder == LATIN && coder == UTF16
    StringLatin1.inflate(value, 0, dst, dstBegin, value.length);
  }
}

Что важно? Если строки однородны, то для перемещения данных используется интринсифицированный System::arraycopy, в противоположном случае — StringLatin1::inflate, которые через делегирование приводит нас к методу StringUTF16::inflate:

// inflatedCopy byte[] -> byte[]
@HotSpotIntrinsicCandidate
public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) {
  // We need a range check here because 'putChar' has no checks
  checkBoundsOffCount(dstOff, len, dst);
  for (int i = 0; i < len; i++) {
    putChar(dst, dstOff++, src[srcOff++] & 0xff);
  }
}

@HotSpotIntrinsicCandidate
static void putChar(byte[] val, int index, int c) {
  assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
  index <<= 1;
  val[index++] = (byte)(c >> HI_BYTE_SHIFT);
  val[index]   = (byte)(c >> LO_BYTE_SHIFT);
}

Таким образом, если строки однородны, то для перемещения данных используется платформенно-зависимый метод System::arraycopy, в противном случае — цикл (тоже интринсифицированный). Это означает, что при склеивании двух строк, где все знаки входят в множество основной латиницы (проще говоря умещаются в 1 байт), производительность должна быть значительно лучше, чем при склеивании разнородных строк. Бенчмарк это подтверждает (см. вывод для nonLatin = false).

Теперь метод AbstractStringBuilder.append(CharSequence, int, int):

@Override
public AbstractStringBuilder append(CharSequence s, int start, int end) {
  if (s == null) {
    s = "null";
  }
  checkRange(start, end, s.length());
  int len = end - start;
  ensureCapacityInternal(count + len);
  appendChars(s, start, end);
  return this;
}

private final void appendChars(CharSequence s, int off, int end) {
  if (isLatin1()) {
    byte[] val = this.value;
    for (int i = off, j = count; i < end; i++) {
      char c = s.charAt(i);
      if (StringLatin1.canEncode(c)) {
        val[j++] = (byte)c;
      } else {
        count = j;
        inflate();
        StringUTF16.putCharsSB(this.value, j, s, i, end);
        count += end - i;
        return;
      }
    }
  } else {
    StringUTF16.putCharsSB(this.value, count, s, off, end);
  }
  count += end - off;
}

Здесь подход схож с существующим в предыдущем примере: для однородных строк используется более простой механизм (тут — познаковое копирование в цикле), для разнородных — обращение к StringUTF16, однако обратите внимание, что вызываемый метод StringUTF16::putCharsSB не интринсифицирован.

public static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) {
    checkBoundsBeginEnd(index, index + end - off, val);
    for (int i = off; i < end; i++) {
        putChar(val, index++, s.charAt(i));
    }
}

Итак, внутреннее устройство обоих методов и причина их разной производительности для нас более-менее понятны. Закономерно возникает вопрос — что делать с полученным знанием дальше? Есть сразу несколько вариантов:

1) держать это в голове и при обнаружении подозрительного кода менять его руками
2) сходить к Тагиру и попросить его запилить проверку, которая будет делать работу вместо нас
3) внести изменения в JDK с тем, чтобы код вообще не менять.

Разумеется, начнём мы с третьего. Готовы рискнуть?


Погружение в пучину

Тренироваться будем на кошках на исходниках одинадцатой явы, скачать можно здесь: http://hg.openjdk.java.net/jdk/jdk11

Наиболее простой и очевидный способ улучшения — выделять подстроку уже внутри метода AbstractStringBuilder.append(CharSequence, int, int):

// было
public AbstractStringBuilder append(CharSequence s, int start, int end) {
  if (s == null) {
    s = "null";
  }
  checkRange(start, end, s.length());
  int len = end - start;
  ensureCapacityInternal(count + len);
  appendChars(s, start, end);
  return this;
}

// стало
public AbstractStringBuilder append(CharSequence s, int start, int end) {
    if (s == null) {
        s = "null";
    }
    checkRange(start, end, s.length());
    return append(s.subSequence(start, end).toString());
}

Теперь нужно собрать JDK, прогнать тесты и запустить на нём бенчмарк StringBuilderAppendBenchmark::appendBounds, результаты которого нужно сопоставить с результатами этого же бенчмарка на исходном JDK:

# здесь колонка before содержит данные прогона на исходном JDK,
# after - на изменённом

Benchmark           nonLatin  length     before      after    Units

avgt                    true       5       44,6       64,4    ns/op
avgt                    true      10       45,7       66,3    ns/op
avgt                    true      50      129,0      168,9    ns/op
avgt                    true     100      218,7      281,9    ns/op
avgt                    true     500      907,1     1116,2    ns/op
avgt                    true    1000     1626,4     2002,5    ns/op

gc.alloc.rate.norm      true       5      184,0      264,0     B/op
gc.alloc.rate.norm      true      10      200,0      296,0     B/op
gc.alloc.rate.norm      true      50      688,0      904,0     B/op
gc.alloc.rate.norm      true     100     1192,0     1552,0     B/op
gc.alloc.rate.norm      true     500     5192,0     6752,0     B/op
gc.alloc.rate.norm      true    1000    10200,0    13256,0     B/op

avgt                   false       5       20,8       38,0    ns/op
avgt                   false      10       24,0       37,8    ns/op
avgt                   false      50       66,4       82,9    ns/op
avgt                   false     100      111,0      138,8    ns/op
avgt                   false     500      419,2      531,9    ns/op
avgt                   false    1000      840,4     1002,7    ns/op

gc.alloc.rate.norm     false       5       80,0      152,0     B/op
gc.alloc.rate.norm     false      10       88,0      168,0     B/op
gc.alloc.rate.norm     false      50      320,0      440,0     B/op
gc.alloc.rate.norm     false     100      544,0      688,0     B/op
gc.alloc.rate.norm     false     500     2144,0     2688,0     B/op
gc.alloc.rate.norm     false    1000     4152,0     5192,0     B/op

Что называется, внезапно! Улучшения не только не произошло, но произошло ухудшение. Чёрт возьми, но как?

Дело в том, что в самом начале, в описании метода StringBuilder::append я сделал одно небольшое, но критически важное упущение. Метод был описан вот так:

public final class StringBuilder {
  @Override
  public StringBuilder append(String str) {
    super.append(str);
    return this;
  }
}

А вот его полный вид:

public final class StringBuilder {
  @Override
  @HotSpotIntrinsicCandidate
  public StringBuilder append(String str) {
    super.append(str);
    return this;
  }
}

Ява-код, который мы рассматривали выше, будучи разогретым и скомпилированным на уровне С2, не имеет никакого значения, т. к. исполняется не он, а интринсик. Это легко доказать, сняв профиль используя async-profiler. Здесь и далее профиль снимается для length = 1000 и nonLatin = true:

# профиль метода `appendSubString`, JDK без наших изменений

          ns  percent  samples  top
  ----------  -------  -------  ---
 19096340914   43.57%  1897673  jbyte_disjoint_arraycopy   <---------
 13500185356   30.80%  1343343  jshort_disjoint_arraycopy  <---------
  4124818581    9.41%   409533  java.lang.String.           # создание подстроки
  2177311938    4.97%   216375  java.lang.StringUTF16.compress    # создание подстроки
  1557269661    3.55%   154253  java.util.Arrays.copyOfRange      # создание подстроки
   349344451    0.80%    34823  appendSubString_avgt_jmhStub
   279803769    0.64%    27862  java.lang.StringUTF16.newString
   274388920    0.63%    27312  org.openjdk.jmh.infra.Blackhole.consume
   160962540    0.37%    15946  SpinPause
   122418222    0.28%    11795  __memset_avx2

Кодом StringBuilder-а (да и AbstractStringBuilder-а ) тут даже не пахнет, почти ¾ профиля занимает интринсик. Примерно такую же картину хотелось бы наблюдать в профиле «улучшенного» нами StringBuilder.append(CharSequence, int, int).

В действительности имеем вот это:

          ns  percent  samples  top
  ----------  -------  -------  ---
 19071221451   43.78%  1897827  jbyte_disjoint_arraycopy
  6409223440   14.71%   638348  jlong_disjoint_arraycopy
  3933622128    9.03%   387403  java.lang.StringUTF16.newBytesFor
  2067248311    4.75%   204193  java.lang.AbstractStringBuilder.ensureCapacityInternal
  1929218737    4.43%   194751  java.lang.StringUTF16.compress
  1678321343    3.85%   166458  java.util.Arrays.copyOfRange
  1621470408    3.72%   160849  java.lang.String.checkIndex
   969180099    2.22%    96018  java.util.Arrays.copyOf
   581600786    1.34%    57818  java.lang.AbstractStringBuilder.
   417818533    0.96%    41611  appendBounds_jmhTest
   406565329    0.93%    40479  java.lang.String.
   340972882    0.78%    33727  java.lang.AbstractStringBuilder.append
   299895915    0.69%    29982  java.lang.StringBuilder.toString
   183885595    0.42%    18136  SpinPause
   168666033    0.39%    16755  org.openjdk.jmh.infra.Blackhole.consume

Вы скажете: «Вот же они, интринсики, в самом верху!» Действительно, только это немного не те интринсики (в т. ч. сравните имя второго сверху). Вспомним:

public final class StringBuilder {
  @Override
  @HotSpotIntrinsicCandidate
  public StringBuilder append(String str) {
    super.append(str);
    return this;
  }
}

Здесь интринсик подменяет вызов StringBuilder.append(String), но в нашем-то патче этого вызова нет! Вызывается AbstractStringBuilder.append(String). Наблюдаемый нами вызов jbyte_disjoint_arraycopy — это интринсик для StringLatin1::inflate, вызываемого из AbstractStringBuider::putStringAt через String::getBytes. Т. е. в отличии от StringBuilder::append отрабатывает не только платформенно-зависимый, но и ява-код,

С причиной провала разобрались, попробуем добиться успеха иначе. Несложно догадаться, что нам необходимо каким-то образом обратиться именно к StringBuilder::append. Сделать это можно оторвав предыдущую заплатку и внеся изменения в сам StringBuilder:

public final class StringBuilder {
  // было
  @Override
  public StringBuilder append(CharSequence s, int start, int end) {
    super.append(s, start, end);
    return this;
  }
  // стало
  @Override
  public StringBuilder append(CharSequence s, int start, int end) {
    if (s == null) {
      s = "null";
    }
    checkRange(start, end, s.length());
    return this.append(s.subSequence(start, end).toString());
  }
}

Вот теперь всё сделано по уму: вызывается интринсифицированный StringBuilder: append.
Пересобираем, запускаем, сравниваем:

# здесь колонка before содержит данные прогона на исходном JDK,
# after - на изменённом

Benchmark             nonLatin   length    before     after   Units

avgt                      true        5      44,6      60,2   ns/op
avgt                      true       10      45,7      59,1   ns/op
avgt                      true       50     129,0     164,6   ns/op
avgt                      true      100     218,7     276,2   ns/op
avgt                      true      500     907,1    1088,8   ns/op
avgt                      true     1000    1626,4    1959,4   ns/op

gc.alloc.rate.norm        true        5     184,0     264,0    B/op
gc.alloc.rate.norm        true       10     200,0     296,0    B/op
gc.alloc.rate.norm        true       50     688,0     904,0    B/op
gc.alloc.rate.norm        true      100    1192,0    1552,0    B/op
gc.alloc.rate.norm        true      500    5192,0    6752,0    B/op
gc.alloc.rate.norm        true     1000   10200,0   13256,0    B/op

avgt                     false        5      20,8      37,9   ns/op
avgt                     false       10      24,0      37,9   ns/op
avgt                     false       50      66,4      80,9   ns/op
avgt                     false      100     111,0     125,6   ns/op
avgt                     false      500     419,2     483,6   ns/op
avgt                     false     1000     840,4     893,8   ns/op

gc.alloc.rate.norm       false        5      80,0     152,0    B/op
gc.alloc.rate.norm       false       10      88,0     168,0    B/op
gc.alloc.rate.norm       false       50     320,0     440,0    B/op
gc.alloc.rate.norm       false      100     544,0     688,0    B/op
gc.alloc.rate.norm       false      500    2144,0    2688,0    B/op
gc.alloc.rate.norm       false     1000    4152,0    5187,2    B/op

Мне, право, очень грустно, но лучше не стало. Теперь новый профиль:

          ns  percent  samples  top
  ----------  -------  -------  ---
 19614374885   44.12%  1953620  jbyte_disjoint_arraycopy
  6645299702   14.95%   662146  jlong_disjoint_arraycopy
  4065789919    9.15%   400167  java.lang.StringUTF16.newBytesFor
  2374627822    5.34%   234746  java.lang.AbstractStringBuilder.ensureCapacityInternal
  1837858014    4.13%   183822  java.lang.StringUTF16.compress
  1472039604    3.31%   145956  java.util.Arrays.copyOfRange
  1316397864    2.96%   130747  appendBounds_jmhTest
   956823151    2.15%    94959  java.util.Arrays.copyOf
   573091712    1.29%    56933  java.lang.AbstractStringBuilder.
   434454076    0.98%    43202  java.lang.String.
   368480388    0.83%    36439  java.lang.AbstractStringBuilder.append
   304409057    0.68%    30442  java.lang.StringBuilder.toString
   272437989    0.61%    26833  SpinPause
   201051696    0.45%    19985  java.lang.StringBuilder.
   198934052    0.45%    19810  appendBounds_avgt_jmhStub

Мало что изменилось. Для меня остаётся непонятным, почему интринсик не сработал при обращении к StringBuilder.append(String) изнутри StringBuilder-а. Есть подозрение, что вклеивание (инлайнинг) тела метода StringBuilder.append(String) в тело StringBuilder.append(CharSequence, int, int) что-то меняет в обработке ВМ обращений к методам.

Как бы там ни было, это фиаско, братан. Подлатать JDK не удалось, но мы по прежнему можем делать замену вручную там, где это имеет смысл.


Литературное отступление о неудачах

Ответная шифровка пришла через два дня. Навигатору совсем не хочется расставаться с «Ото Велара», с фирмой, которая строит удивительно быстрые и мощные военные корабли. Навигатору не хочется читать шифровку мне. Он просто повторяет ответ командного пункта: «Нет». Шифровка не разъясняет, почему «нет». «Нет» в любом случае означает, что он –личность известная большому компьютеру. Если бы о нем ничего не было известно, то ответ был бы положительным: пробуйте. Жаль. Жаль такого, интересного человека терять. А командиру, наверное, жаль меня. Может быть, первый раз жаль. Он видит, что я рвусь в варяги. И ему совсем не хочется вновь толкать меня в борзые.
Он молчит. Но я-то знаю, что в обеспечении дикая нехватка рабочих рук:
— Я, товарищ генерал, завтра в обеспечении работаю. Разрешите идти?
— Иди. — И вдруг улыбается. — Ты знаешь, нет худа без добра.
— У меня, товарищ генерал, всегда худо без добра.
— А вот и нет. Тебе запретили его встречать, это плохо. Но к сокровищам нашего опыта мы добавили еще одну крупицу.

Выводы:


  • код методов JDK в некоторых случаях не имеет отношения к настоящему исполнению, т. к. вместо тела метода может выполнится интринсик, который укрыт в недрах ВМ.
  • подобные методы можно распознать, в частности на них указывает метка @HotSpotIntrinsicCandidate, хотя некоторые методы интринсифицированы без каких бы то ни было намёков, например String::equals (и многие-многие другие)
  • вывод, проистекающий из первых двух, — наши рассуждения о том, как работает код JDK могут противоречить действительности. C’est la vie

P. S.
Другая возможная замена:

StringBuilder sb = new StringBuilder();
sb.append(str, 0, endIndex);
// -->
StringBuilder sb = new StringBuilder(str.substring(o, endIndex));

P.P. S.
Разработчики «Оракла» справедливо указывают, что


It seems to me rather odd and surprising to introduce a code path into
sb.append (cs, int, int) that allocates memory in order to get at an intrinsic that
only sometimes makes things run faster. As you observed, the performance
tradeoffs aren’t obvious.

Instead, if we want to optimize sb.append (cs, int, int) maybe we should just go
ahead and do that, possibly by adding or rearranging the intrinsics.

Предложенное решение — интринсификация StringBuilder.append(CharSequence, int, int).
Задача: https://bugs.openjdk.java.net/browse/JDK-8217675
Обсуждение: http://mail.openjdk.java.net/pipermail/core-libs-dev/2019-January/057834.html

P.P. S.
Интересно, что в настоящий момент при написании чего-то вроде

StringBuilder sb = new StringBuilder();
sb.append(str.substring(0, endIndex));

«Идея» предлагает упростить код до

StringBuilder sb = new StringBuilder();
sb.append(s, 0, endIndex);

Если производительность именно в этом месте вам не очень важна, наверно более правильным будет использовать второй, упрощённый вариант. Всё-таки большую часть кода мы пишем для наших товарищей, а не для машин.

© Habrahabr.ru