Подтвердить что ты не робот

Почему "новое" ключевое слово намного эффективнее, чем назначение?

У меня есть два метода для чтения в строке и создание объектов Character:

static void newChar(String string) {
    int len = string.length();
    System.out.println("Reading " + len + " characters");
    for (int i = 0; i < len; i++) {
        Character cur = new Character(string.charAt(i));

    }       
}

и

static void justChar(String string) {
    int len = string.length();
    for (int i = 0; i < len; i++) {
        Character cur = string.charAt(i);

    }
}

Когда я запускаю методы с использованием строки символов 18 554 760, я получаю дико разное время выполнения. Выход, который я получаю:

newChar took: 20 ms
justChar took: 41 ms

При меньшем входе (4 638 690 символов) время не так разнообразно.

newChar took: 12 ms
justChar took: 13 ms

Почему в этом случае новый настолько эффективен?

EDIT:

Мой тестовый код довольно хаки.

start = System.currentTimeMillis();
newChar(largeString);
end = System.currentTimeMillis();
diff = end-start;
System.out.println("New char took: " + diff + " ms");

start = System.currentTimeMillis();
justChar(largeString);
end = System.currentTimeMillis();
diff = end-start;
System.out.println("just char took: " + diff+ " ms");
4b9b3361

Ответ 1

TL; раздел DR

Хорошие новости

Ваше измерение действительно создает реальный эффект.

Плохая новость

Это происходит в основном случайно, потому что ваш тест имеет много технических недостатков, и эффект, который он предоставляет, вероятно, не тот, который вы имеете в виду.

Подход new Character() быстрее, если и только если HotSpot Escape Analysis успешно доказывает, что результирующий экземпляр можно безопасно распределить в стеке вместо кучи. Поэтому эффект не так важен, как подразумевается в вашем вопросе.

Объяснение эффекта

Причина, по которой new Character() быстрее, - это локальность ссылки: ваш экземпляр находится в стеке, и весь доступ к нему осуществляется через образы кэша CPU. Когда вы повторно используете кешированный экземпляр, вы должны

  • получить доступ к удаленному полю static;
  • разыщите его в удаленном массиве;
  • разыменовать запись массива в удаленный экземпляр Character;
  • присоединяется к char, содержащемуся в этом экземпляре.

Каждое разыменование является потенциальной ошибкой кэша процессора. Кроме того, он заставляет часть кэша перенаправляться в эти удаленные местоположения, что приводит к большему количеству промахов в кеше на входной строке и/или в местах расположения стека.

ПОДРОБНОСТИ

Я запустил этот код с помощью jmh:

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class Chars {
  static String string = "12345678901234567890"; static {
    for (int i = 0; i < 10; i++) string += string;
  }

  @GenerateMicroBenchmark
  public void newChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) new Character(string.charAt(i));
  }

  @GenerateMicroBenchmark
  public void justChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
  }
}

Это сохраняет суть вашего кода, но устраняет некоторые систематические ошибки, такие как разминка и время компиляции. Вот результаты:

Benchmark              Mode Thr    Cnt  Sec         Mean   Mean error    Units
o.s.Chars.justChar     avgt   1      3    5       39.062        6.587  usec/op
o.s.Chars.newChar      avgt   1      3    5       19.114        0.653  usec/op

И это было бы моим лучшим предположением о том, что происходит:

  • в newChar вы создаете новый экземпляр Character. Анализ Escape с помощью HotSpot может доказать, что экземпляр никогда не ускользает, поэтому он позволяет распределять стек или в специальном случае Character может полностью исключить выделение, поскольку данные из него, по-видимому, никогда не используются;

  • в justChar вы включаете поиск в массив кеша Character, который имеет некоторую стоимость.

UPDATE

В ответ на критику Алекса, я добавил еще несколько методов к эталону. Основной эффект остается стабильным, но мы получаем еще более мелкие детали о меньших эффектах оптимизации.

  @GenerateMicroBenchmark
  public int newCharUsed() {
    int len = string.length(), sum = 0;
    for (int i = 0; i < len; i++) sum += new Character(string.charAt(i));
    return sum;
  }

  @GenerateMicroBenchmark
  public int justCharUsed() {
    int len = string.length(), sum = 0;
    for (int i = 0; i < len; i++) sum += Character.valueOf(string.charAt(i));
    return sum;
  }

  @GenerateMicroBenchmark
  public void newChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) new Character(string.charAt(i));
  }

  @GenerateMicroBenchmark
  public void justChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
  }

  @GenerateMicroBenchmark
  public void newCharValue() {
    int len = string.length();
    for (int i = 0; i < len; i++) new Character(string.charAt(i)).charValue();
  }

  @GenerateMicroBenchmark
  public void justCharValue() {
    int len = string.length();
    for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i)).charValue();
  }

ОПИСАНИЕ:

  • базовые версии: justChar и newChar;
  • ...Value методы добавляют вызов charValue к базовой версии;
  • ...Used методы добавляют вызов charValue (неявно) и используют значение, чтобы исключить любое исключение Dead Code.

Результаты:

Benchmark                   Mode Thr    Cnt  Sec         Mean   Mean error    Units
o.s.Chars.justChar          avgt   1      3    1      246.847        5.969  usec/op
o.s.Chars.justCharUsed      avgt   1      3    1      370.031       26.057  usec/op
o.s.Chars.justCharValue     avgt   1      3    1      296.342       60.705  usec/op
o.s.Chars.newChar           avgt   1      3    1      123.302       10.596  usec/op
o.s.Chars.newCharUsed       avgt   1      3    1      172.721        9.055  usec/op
o.s.Chars.newCharValue      avgt   1      3    1      123.040        5.095  usec/op
  • есть свидетельства об исключении Dead Code Elimination (DCE) как в вариантах justChar, так и newChar, но это только частичное;
  • с вариантом newChar, добавление charValue не имеет никакого эффекта, поэтому, по-видимому, это был DCE'd;
  • с justChar, charValue имеет эффект, поэтому, похоже, он не был устранен;
  • DCE имеет незначительный общий эффект, о чем свидетельствует устойчивая разница между newCharUsed и justCharUsed.

Ответ 2

Ну, я не уверен, что Марко был намерен повторить оригинальную ошибку. TL; DR; новый экземпляр не используется, его устраняют. Регулировка эталона меняет результат. Не доверяйте ошибочным критериям, учитесь у них.

Здесь тестовый показатель JMH:

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3, time = 1)
@Fork(3)
@State(Scope.Thread)
public class Chars {

    // Source needs to be @State field to avoid constant optimizations
    // on sources. Results need to be sinked into the Blackhole to
    // avoid dead-code elimination
    private String string;

    @Setup
    public void setup() {
        string = "12345678901234567890";
        for (int i = 0; i < 10; i++) {
            string += string;
        }
    }

    @GenerateMicroBenchmark
    public void newChar_DCE(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            Character c = new Character(string.charAt(i));
        }
    }

    @GenerateMicroBenchmark
    public void justChar_DCE(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            Character c = Character.valueOf(string.charAt(i));
        }
    }

    @GenerateMicroBenchmark
    public void newChar(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            Character c = new Character(string.charAt(i));
            bh.consume(c);
        }
    }

    @GenerateMicroBenchmark
    public void justChar(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            Character c = Character.valueOf(string.charAt(i));
            bh.consume(c);
        }
    }

    @GenerateMicroBenchmark
    public void newChar_prim(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            char c = new Character(string.charAt(i));
            bh.consume(c);
        }
    }

    @GenerateMicroBenchmark
    public void justChar_prim(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            char c = Character.valueOf(string.charAt(i));
            bh.consume(c);
        }
    }
}

... и это результат:

Benchmark                   Mode   Samples         Mean   Mean error    Units
o.s.Chars.justChar          avgt         9       93.051        0.365    us/op
o.s.Chars.justChar_DCE      avgt         9       62.018        0.092    us/op
o.s.Chars.justChar_prim     avgt         9       82.897        0.440    us/op
o.s.Chars.newChar           avgt         9      117.962        4.679    us/op
o.s.Chars.newChar_DCE       avgt         9       25.861        0.102    us/op
o.s.Chars.newChar_prim      avgt         9       41.334        0.183    us/op

DCE означает "Dead Code Elimination", и от этого страдает оригинальный критерий. Если мы устраним этот эффект, то в способе JMH это потребует, чтобы мы опустили значения в Blackhole, оценка изменилась. Таким образом, ретроспективно, похоже, что new Character() в исходном коде имеет значительное улучшение с DCE, а Character.valueOf не так успешна. Я не уверен, что мы должны обсудить, почему, потому что это не имеет отношения к реальным случаям использования в мире, где фактически используются персонажи.

Вы можете пойти дальше на двух фронтах отсюда:

  • Получите сборку для эталонных методов, чтобы подтвердить гипотезу выше. См. PrintAssembly.
  • Выполнить с большим количеством потоков. Разница между возвратом кэшированного символа и созданием нового будет уменьшаться по мере увеличения количества потоков и, следовательно, попадания в "стену распределения".

UPD: Следуя за вопросом Марко, похоже, что основное влияние на устранение самого распределения, будь то через EA или DCE, см. в тестах * _prim.

UPD2: Посмотрел на сборку. Тот же самый запуск с -XX:-DoEscapeAnalysis подтверждает, что основной эффект связан с устранением выделения, поскольку эффект анализа escape:

Benchmark                   Mode   Samples         Mean   Mean error    Units
o.s.Chars.justChar          avgt         9       94.318        4.525    us/op
o.s.Chars.justChar_DCE      avgt         9       61.993        0.227    us/op
o.s.Chars.justChar_prim     avgt         9       82.824        0.634    us/op
o.s.Chars.newChar           avgt         9      118.862        1.096    us/op
o.s.Chars.newChar_DCE       avgt         9       97.530        2.485    us/op
o.s.Chars.newChar_prim      avgt         9      101.905        1.871    us/op

Это доказывает, что исходная гипотеза DCE неверна. EA является основным вкладчиком. Результаты DCE все еще быстрее, потому что мы не оплачиваем затраты на распаковку и обычно обрабатываем возвращаемое значение с любым уважением. Тем не менее контрольный показатель неисправен в этом отношении.