Описание производительности: код работает быстрее с неиспользуемой переменной

Раньше я выполнял некоторые тесты производительности и не могу объяснить полученные результаты.

При выполнении теста ниже, если я раскомментирую private final List<String> list = new ArrayList<String>();, производительность значительно улучшится. На моей машине тест проходит в течение 70-90 мс, когда это поле присутствует против 650 мс, когда он закомментирован.

Я также заметил, что если я изменил оператор печати на System.out.println((end - start) / 1000000);, тест без переменной пробегает 450-500 мс вместо 650 мс. Он не действует, когда присутствует переменная.

Мои вопросы:

  • Кто-нибудь может объяснить коэффициент почти 10 с переменной или без нее, учитывая, что я даже не использую эту переменную?
  • Как этот оператор печати может изменить производительность (особенно, поскольку он появляется после окна измерения производительности)?

ps: при запуске последовательно три сценария (с переменной, без переменной, с другим выражением на печать) занимают около 260 мс.

public class SOTest {

    private static final int ITERATIONS = 10000000;
    private static final int THREADS = 4;

    private volatile long id = 0L;
    //private final List<String> list = new ArrayList<String>();

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(THREADS);
        final List<SOTest> objects = new ArrayList<SOTest>();
        for (int i = 0; i < THREADS; i++) {
            objects.add(new SOTest());
        }

        //warm up
        for (SOTest t : objects) {
            getRunnable(t).run();
        }

        long start = System.nanoTime();

        for (SOTest t : objects) {
            executor.submit(getRunnable(t));
        }
        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);

        long end = System.nanoTime();
        System.out.println(objects.get(0).id + " " + (end - start) / 1000000);
    }

    public static Runnable getRunnable(final SOTest object) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < ITERATIONS; i++) {
                    object.id++;
                }
            }
        };
        return r;
    }
}

EDIT

Ниже приведены результаты 10 запусков с тремя сценариями:

  • без переменной, используя оператор короткой печати
  • без переменной, используя оператор long print (печатает один из объектов)
  • последовательный запуск (1 поток)
  • с переменной
1   657 473 261 74
2   641 501 261 78
3   651 465 259 86
4   585 462 259 78
5   639 506 259 68
6   659 477 258 72
7   653 479 259 82
8   645 486 259 72
9   650 457 259 78
10  639 487 272 79

Ответы

Ответ 1

Очистить (false) общий доступ

из-за макета в памяти объекты обходят строки кеша... Это объяснялось много раз (даже на этом сайте): вот хороший источник для дальнейшего чтения. Эта проблема применима к С# столько же (или C/С++)

Когда вы накладываете объект, добавляя прокомментированную строку, общий доступ меньше, и вы видите повышение производительности.

Изменить: я пропустил второй вопрос:


Как этот оператор печати может изменить производительность (тем более, что он появляется после окна измерения производительности)?

Я думаю, что недостаточно нагревания, распечатайте журналы GC и компиляции, чтобы вы могли быть уверены, что нет помех, и код действительно скомпилирован. java -server требуется 10k итераций, желательно не всех в основном цикле, чтобы генерировать хороший код.

Ответ 2

Вы получаете тонкий эффект от выполняемого оборудования. Объекты SOTest очень малы в памяти, поэтому все 4 экземпляра могут входить в одну и ту же строку кэша в памяти. Поскольку вы используете volatile, это приведет к сбою кеша между различными ядрами (только одно ядро ​​может иметь грязную линию кэша).

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

Доказательство. Прокомментируйте ArrayList и поставьте вместо этого:

long waste1, waste2, waste3, waste4, waste5, waste6, waste7, waste8;

Это увеличивает ваши SOTest-объекты на 64 байта (размер одной строки кэша на процессорах pentium). Производительность теперь такая же, как при использовании ArrayList.

Ответ 3

Это всего лишь идея, и я понятия не имею, как ее проверить, но это может быть связано с кешированием. При наличии ArrayList ваши объекты становятся намного крупнее, поэтому меньшее количество из них вписывается в некоторую заданную область кэшированной памяти, что приводит к большему количеству промахов в кеше.

В действительности вы можете попробовать использовать ArrayLists разного размера, тем самым изменяя размер памяти ваших экземпляров класса и проверяя, влияет ли это на производительность.

Ответ 4

Довольно интересное путешествие. Это скорее "ответ на мои результаты". Я подозреваю/надеюсь, что другие придумают лучшие ответы.

Вы, очевидно, сталкиваетесь с некоторыми интересными точками оптимизации. Я подозреваю, что добавление objects.get(0).id в длинном println выражает удаление некоторых оптимизаций вокруг использования поля id. Помимо ++ нет другого использования id, поэтому оптимизатор оптимизирует некоторое количество обращений к volatile id, что приводит к улучшению скорости. Простое обращение к полю id с long x = objects.get(0).id; приводит к такому же повышению производительности.

Поле List гораздо интереснее. Такое же улучшение производительности происходит, если добавлено поле private String foo = new String("weofjwe");, но не если оно private String foo = "weofjwe";, которое не создает объект, так как "..." выполняется во время компиляции. Я был уверен, что final имеет значение, но, похоже, это не так. Я могу только предположить, что это имеет какое-то отношение к оптимизации конструктора с добавлением new, заставляя оптимизацию останавливаться, хотя я бы хотя бы volatile сделал бы это более эффективно.