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

Распределение и доступ к массиву на виртуальной машине Java и конфликте памяти

Соблюдайте следующее определение подкласса потока (весь исходный исходный файл Java включен в конце вопроса для вашего удобства):

final class Worker extends Thread {
    Foo[] array = new Foo[1024];
    int sz;

    public Worker(int _sz) {
        sz = _sz;
    }

    public void run() {
        //Foo[] arr = new Foo[1024];
        Foo[] arr = array;
        loop(arr);
    }

    public void loop(Foo[] arr) {
        int i = 0;
        int pos = 512;
        Foo v = new Foo();
        while (i < sz) {
            if (i % 2 == 0) {
                arr[pos] = v;
                pos += 1;
            } else {
                pos -= 1;
                v = arr[pos];
            }
            i++;
        }
    }
}

Объяснение: программа запускает -Dpar такие потоки и устанавливает sz каждого потока на -Dsize / -Dpar, где -Dsize и -Dpar устанавливаются через командную строку, когда запуск программы. Каждый объект потока имеет поле array, которое инициализируется новым массивом 1024 -element. Причиной является то, что мы хотим разделить равный объем работы между различным количеством потоков - мы ожидаем, что программа будет масштабироваться.

Затем запускается каждый поток и измеряется время, необходимое для завершения всех потоков. Мы выполняем несколько измерений для противодействия любым связанным с JIT эффектам, как показано ниже. Каждый поток выполняет цикл. Внутри цикла поток читает элемент в позиции 512 в массиве в четных итерациях и записывает тот же элемент в 512 в нечетные итерации. В противном случае изменяются только локальные переменные.

Полная программа ниже.

Анализ:

Протестировано с помощью -verbose:gc - сбор мусора, возникающий во время запуска этой программы.

Команда запуска:

java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 org.scalapool.bench.MultiStackJavaExperiment 7

CASE 1: Время выполнения для 1,2,4,8 потоков в этом порядке (7 повторений):

>>> All running times: [2149, 2227, 1974, 1948, 1803, 2283, 1878]
>>> All running times: [1140, 1124, 2022, 1141, 2028, 2004, 2136]
>>> All running times: [867, 1022, 1457, 1342, 1436, 966, 1531]
>>> All running times: [915, 864, 1245, 1243, 948, 790, 1007]

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

CASE 2: Далее я комментирую строку Foo[] arr = array в методе run потока и выделяю новый массив в самом методе run: Foo[] arr = new Foo[1024]. Размеры:

>>> All running times: [2053, 1966, 2089, 1937, 2046, 1909, 2011]
>>> All running times: [1048, 1178, 1100, 1194, 1367, 1271, 1207]
>>> All running times: [578, 508, 589, 571, 617, 643, 645]
>>> All running times: [330, 299, 300, 322, 331, 324, 575]

На этот раз все масштабируется так, как ожидалось. Я бы не подумал, что место, где был выделен массив, играет какую-то роль, но, очевидно, это как-то. Моя мысль заключалась в том, что массивы ранее были распределены так близко друг к другу, что возникло конфликт памяти.

CASE 3: Чтобы проверить это предположение, я снова раскомментировал строку Foo[] arr = array, но на этот раз инициализировал поле array на new Foo[32000], чтобы гарантировать, что местоположение в памяти, которое записывается, достаточно далеко от каждого Другие. Итак, здесь мы снова используем массив, выделенный при создании объекта потока, разница с CASE1 заключается только в том, что массив больше.

>>> All running times: [2113, 1983, 2430, 2485, 2333, 2359, 2463]
>>> All running times: [1172, 1106, 1163, 1181, 1142, 1169, 1188]
>>> All running times: [578, 677, 614, 604, 583, 637, 597]
>>> All running times: [343, 327, 320, 330, 353, 320, 320]

Таким образом, проблема памяти является причиной этого.

Информация о платформе:

Ubuntu Server 10.04.3 LTS
8 core Intel(R) Xeon(R) CPU  X5355  @2.66GHz
~20GB ram
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)

Вопрос. Это, очевидно, проблема со стороны памяти. Но почему это происходит?

  • Проводится ли анализ эвакуации? Если да, значит ли это, что весь массив выделяется в стеке при создании в методе run в CASE2? Каковы точные условия для этой оптимизации времени выполнения? Разумеется, массив не выделяется в стек за 1 миллион элементов?

  • Даже если массив выделяется в стеке, а не выделяется на куча, два обращения к массиву различными потоками должны быть разделены по меньшей мере на 512 * 4 байта = 2 КБ, даже в CASE1, где бы ни были массивы! Это определенно больше, чем любая линия кеша L1. Если эти эффекты происходят из-за ложного обмена, то как записи на несколько полностью независимых строк кеша влияют на производительность? (Одно из предположений здесь состоит в том, что каждый массив занимает смежный блок памяти на JVM, который выделяется при создании массива. Я не уверен, что это действительно. Другое предположение заключается в том, что массивные записи не проходят весь путь до памяти, но вместо L1-кеша, поскольку у Intel Xeon есть архитектура ccNUMA - исправьте меня, если я ошибаюсь)

  • Возможно ли, что каждый поток имеет свою собственную локальную кучную часть, где он самостоятельно выделяет новые объекты, и это является причиной более низкой конкуренции, когда массив выделен в потоке? Если да, то как эта область кучного мусора собирается, если ссылки разделяются?

  • Почему увеличение размера массива до ~ 32000 элементов улучшило масштабируемость (сокращение памяти)? Что именно в иерархии памяти является причиной этого?

Просьба быть точным и поддерживать ваши претензии со ссылками.

Спасибо!


Вся исполняемая Java-программа:

import java.util.ArrayList;

class MultiStackJavaExperiment {

    final class Foo {
        int x = 0;
    }

    final class Worker extends Thread {
        Foo[] array = new Foo[1024];
        int sz;

        public Worker(int _sz) {
            sz = _sz;
        }

        public void run() {
            Foo[] arr = new Foo[1024];
            //Foo[] arr = array;
            loop(arr);
        }

        public void loop(Foo[] arr) {
            int i = 0;
            int pos = 512;
            Foo v = new Foo();
            while (i < sz) {
                if (i % 2 == 0) {
                    arr[pos] = v;
                    pos += 1;
                } else {
                    pos -= 1;
                    v = arr[pos];
                }
                i++;
            }
        }
    }

    public static void main(String[] args) {
        (new MultiStackJavaExperiment()).mainMethod(args);
    }

    int size = Integer.parseInt(System.getProperty("size"));
    int par = Integer.parseInt(System.getProperty("par"));

    public void mainMethod(String[] args) {
        int times = 0;
        if (args.length == 0) times = 1;
        else times = Integer.parseInt(args[0]);
        ArrayList < Long > measurements = new ArrayList < Long > ();

        for (int i = 0; i < times; i++) {
            long start = System.currentTimeMillis();
            run();
            long end = System.currentTimeMillis();

            long time = (end - start);
            System.out.println(i + ") Running time: " + time + " ms");
            measurements.add(time);
        }

        System.out.println(">>>");
        System.out.println(">>> All running times: " + measurements);
        System.out.println(">>>");
    }

    public void run() {
        int sz = size / par;
        ArrayList < Thread > threads = new ArrayList < Thread > ();

        for (int i = 0; i < par; i++) {
            threads.add(new Worker(sz));
            threads.get(i).start();
        }
        for (int i = 0; i < par; i++) {
            try {
                threads.get(i).join();
            } catch (Exception e) {}
        }
    }

}
4b9b3361

Ответ 1

Решение

Запустите JVM с флагом -XX:+UseCondCardMark, доступным только в JDK7. Это решает проблему.

Объяснение

По сути, в большинстве средах с управляемыми кучами используются карточные таблицы, чтобы отметить области памяти, в которые были сделаны записи. Такие области памяти отмечены как загрязненные в таблице карт после записи. Эта информация необходима для сбора мусора. Не нужно сканировать ссылки на не грязные области памяти. Карта представляет собой непрерывный блок памяти, обычно 512 байт. Карточный стол обычно имеет 1 байт для каждой карты - если этот байт установлен, карта загрязнена. Это означает, что карточный стол с 64 байтами охватывает 64 * 512 байт памяти. И, как правило, размер строки кэша сегодня составляет 64 байта.

Таким образом, каждый раз, когда возникает запись в поле объекта, байт соответствующей карты в таблице карт должен быть установлен как грязный. Полезная оптимизация в программах с одним потоком заключается в том, чтобы просто пометить соответствующий байт - писать каждый раз. Альтернатива первой проверки, установлен ли байт, и условная запись требует дополнительного чтения и условного перехода, который немного медленнее.

Однако эта оптимизация может быть катастрофической в ​​том случае, если в память записано несколько процессоров, так как соседние карты записываются так, чтобы требовать записи в соседние байты в таблице карт. Таким образом, область памяти, на которую записывается (запись в массиве выше), не находится в одной и той же линии кэша, что является обычной причиной конфликта памяти. Настоящая причина в том, что грязные байты, которые записаны, находятся в одной строке кэша.

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

Я заметил, что этот флаг увеличивает время работы около 15-20% в случае с 1 потоком.

Флаг -XX:+UseCondCardMark объясняется в этом сообщении , и этот отчет об ошибках.

Соответствующее обсуждение списка рассылки concurrency: Распределение и доступ к массиву на JVM.

Ответ 2

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

Если вы минимизируете свой код, повторно используйте свои потоки, чтобы вы не останавливали/не запускали их, вы получаете гораздо более воспроизводимые результаты.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MultiStackJavaExperiment {
    static final int size = Integer.getInteger("size", 500000000);

    public static void main(String... args) throws ExecutionException, InterruptedException {
        int par = 8;
        for (int s = 64; s <= 64 * 1024; s *= 2) {
            int times = args.length == 0 ? 1 : Integer.parseInt(args[0]);
            long[] measurements = new long[times];

            ExecutorService es = Executors.newFixedThreadPool(par);
            List<Future<?>> futures = new ArrayList<Future<?>>(times);
            for (int i = 0; i < times; i++) {
                long start = System.currentTimeMillis();
                final int sz = size / par;
                futures.clear();
                for (int j = 0; j < par; j++) {
                    final Object[] arr = new Object[s];
                    futures.add(es.submit(new Runnable() {
                        @Override
                        public void run() {
                            final int bits = 7, arraySize = 1 << bits;
                            int i = 0;
                            int pos = 32;
                            Object v = new Object();
                            while (i < sz) {
                                if (i % 2 == 0) {
                                    arr[pos] = v;
                                    pos += 1;
                                } else {
                                    pos -= 1;
                                    v = arr[pos];
                                }
                                i++;
                            }
                        }
                    }));
                }
                for (Future<?> future : futures)
                    future.get();

                long time = System.currentTimeMillis() - start;
//                System.out.println(i + ") Running time: " + time + " ms");
                measurements[i] = time;
            }
            es.shutdown();
            System.out.println("par = " + par + " arr.length= "+ s  + " >>> All running times: " + Arrays.toString(measurements));
        }
    }
}

это показывает расстояние между значениями доступа. Путем выделения массива каждый поток, вы используете разные TLAB (которые выделяют данные в блоках)

par = 8 arr.length= 64 >>> All running times: [539, 413, 444, 444, 457, 444, 456]
par = 8 arr.length= 256 >>> All running times: [398, 527, 514, 529, 445, 441, 445]
par = 8 arr.length= 1024 >>> All running times: [419, 507, 477, 422, 412, 452, 396]
par = 8 arr.length= 4096 >>> All running times: [316, 282, 250, 232, 242, 229, 238]
par = 8 arr.length= 16384 >>> All running times: [316, 207, 209, 212, 208, 208, 208]
par = 8 arr.length= 65536 >>> All running times: [211, 211, 208, 208, 208, 291, 206]
par = 8 arr.length= 262144 >>> All running times: [366, 210, 210, 210, 210, 209, 211]
par = 8 arr.length= 1048576 >>> All running times: [296, 211, 215, 216, 213, 211, 211]

если вы перемещаете массив внутри потока, который вы получаете

par = 8 arr.length= 64 >>> All running times: [225, 151, 151, 150, 152, 153, 152]
par = 8 arr.length= 256 >>> All running times: [155, 151, 151, 151, 151, 151, 155]
par = 8 arr.length= 1024 >>> All running times: [153, 152, 151, 151, 151, 155, 152]
par = 8 arr.length= 4096 >>> All running times: [155, 156, 151, 152, 151, 155, 155]
par = 8 arr.length= 16384 >>> All running times: [154, 157, 152, 152, 158, 153, 153]
par = 8 arr.length= 65536 >>> All running times: [155, 157, 152, 184, 181, 154, 153]
par = 8 arr.length= 262144 >>> All running times: [240, 159, 166, 151, 172, 154, 160]
par = 8 arr.length= 1048576 >>> All running times: [165, 162, 163, 162, 163, 162, 163]

Отключите tlab с помощью -XX:-UseTLAB, и тот же код даст syou

par = 8 arr.length= 64 >>> All running times: [608, 467, 467, 457, 468, 461, 428]
par = 8 arr.length= 256 >>> All running times: [437, 437, 522, 512, 522, 369, 535]
par = 8 arr.length= 1024 >>> All running times: [394, 395, 475, 525, 470, 440, 478]
par = 8 arr.length= 4096 >>> All running times: [347, 215, 238, 226, 236, 204, 271]
par = 8 arr.length= 16384 >>> All running times: [291, 157, 178, 151, 150, 151, 152]
par = 8 arr.length= 65536 >>> All running times: [163, 152, 162, 151, 159, 159, 154]
par = 8 arr.length= 262144 >>> All running times: [164, 172, 152, 169, 160, 161, 160]
par = 8 arr.length= 1048576 >>> All running times: [295, 153, 164, 153, 166, 154, 163]