Почему конкатенация строк быстрее, чем String.valueOf для преобразования целого в строку?

У меня есть ориентир:

@BenchmarkMode(Mode.Throughput)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS, batchSize = 1000)
@Measurement(iterations = 40, time = 1, timeUnit = TimeUnit.SECONDS, batchSize = 1000)
public class StringConcatTest {

    private int aInt;

    @Setup
    public void prepare() {
        aInt = 100;
    }

    @Benchmark
    public String emptyStringInt() {
        return "" + aInt;
    }

    @Benchmark
    public String valueOfInt() {
        return String.valueOf(aInt);
    }

}

И вот результат:

Benchmark                                          Mode  Cnt      Score      Error  Units
StringConcatTest.emptyStringInt                   thrpt   40  66045.741 ± 1306.280  ops/s
StringConcatTest.valueOfInt                       thrpt   40  43947.708 ± 1140.078  ops/s

Это показывает, что объединение пустой строки с целым числом на 30% быстрее, чем вызов String.value(100). Я понимаю, что "+ 100 преобразован в

new StringBuilder().append(100).toString()
Оптимизация

и -XX:+OptimizeStringConcat применяется, что делает ее быстрой. Я не понимаю, почему valueOf сам медленнее, чем конкатенация. Может кто-то объяснить, что именно происходит, и почему "+ 100 быстрее. Какую магию делает OptimizeStringConcat?

Ответ 1

Как вы уже упоминали, JSM HotSpot имеет оптимизацию -XX:+OptimizeStringConcat, которая распознает шаблон StringBuilder и заменяет его высоконаправленным рукописным IR-графиком, тогда как String.valueOf() полагается на общие оптимизации компилятора.

Я обнаружил следующие ключевые отличия, проанализировав сгенерированный код сборки:

  • Оптимизированный concat не создает нулевой массив char[] для строки результата, а массив, созданный Integer.toString, очищается после выделения, как и любой другой обычный объект.
  • Оптимизированный concat переводит цифры в символы простым добавлением константы '0', а Integer.getChars использует поиск в таблице с соответствующей оценкой границ массива и т.д.

Существуют и другие незначительные отличия в реализации PhaseStringOpts::int_getChars против Integer.getChars, но Я думаю, они не так важны для производительности.


Кстати, если вы берете большее число (например, 1234567890), разница в производительности будет незначительной из-за дополнительного цикла в Integer.getChars, которая преобразует две цифры в один раз.