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

В какой момент стоит повторно использовать массивы на Java?

Насколько большой буфер должен быть на Java, прежде чем он будет использоваться повторно?

Или, по-другому: я могу многократно выделять, использовать и отбрасывать объекты byte [] или запускать пул для их повторного использования. Я мог бы выделить много маленьких буферов, которые часто отбрасываются, или несколько крупных, которые этого не делают. На какой размер дешевле объединить их, чем перераспределять, и как небольшие распределения сравниваются с большими?

EDIT:

Хорошо, конкретные параметры. Скажем, процессор Intel Core 2 Duo, последняя версия VM для ОС по выбору. Эти вопросы не так расплывчаты, как кажется... маленький код и график могут ответить на него.

EDIT2:

Вы опубликовали много хороших общих правил и обсуждений, но вопрос действительно запрашивает числа. Post 'em (и код тоже)! Теория велик, но доказательство - это числа. Не имеет значения, будут ли результаты отличаться от системы к системе, я просто ищу грубую оценку (по порядку величины). Никто, кажется, не знает, будет ли разница в производительности фактором 1,1, 2, 10 или 100+, и это важно. Это важно для любого Java-кода, работающего с большими массивами - сети, биоинформатики и т.д.

Предложения для хорошего теста:

  • Прогрейте код перед его запуском в эталонном тесте. Методы следует называть как минимум 1000 10000 раз, чтобы получить полную оптимизацию JIT.
  • Удостоверьтесь, что методы сравнения пройдут как минимум 1 10 секунд и, если возможно, используйте System.nanotime, чтобы получить точные тайминги.
  • Запустить тест производительности в системе, которая работает только с минимальными приложениями
  • Запустите тест в 3-5 раз и сообщите все время, поэтому мы видим, насколько он согласован.

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

То, что я знаю (и не нужно повторять):

  • Распределение памяти Java и GC быстро и быстро.
  • Комбинация объектов была хорошей оптимизацией, но теперь она сильно вредит производительности.
  • Объединение объектов "обычно не является хорошей идеей, если объекты не являются дорогими для создания". Ядда ядда.

Что я НЕ знаю:

  • Как быстро я должен ожидать выполнения распределения памяти (МБ/с) на стандартном современном процессоре?
  • Как распределяется эффект распределения размера размещения?
  • Какая точка безубыточности для количества/размера распределений против повторного использования в пуле?

Маршруты для ответа ACCEPTED (тем лучше):

  • Недавний технический документ, показывающий данные о распределении и GC на современных процессорах (последние, как в прошлом году или около того, JVM 1.6 или новее)
  • Код для краткого и правильного микро-теста я могу запустить
  • Объяснение того, как и почему распределения влияют на производительность.
  • Примеры/анекдоты реального мира от тестирования такого рода оптимизации

Контекст:

Я работаю над библиотекой, добавляющей поддержку сжатия LZF для Java. Эта библиотека расширяет классы H2 DBMS LZF, добавляя дополнительные уровни сжатия (большее сжатие) и совместимость с байтовыми потоками из библиотеки C LZF. Одна из вещей, о которых я думаю, заключается в том, стоит ли повторять использование буферов фиксированного размера, используемых для сжатия/распаковки потоков. Буферы могут быть ~ 8 кБ, или ~ 32 кБ, а в исходной версии они ~ 128 кБ. Буферы могут выделяться один или несколько раз на поток. Я пытаюсь понять, как я хочу обрабатывать буферы, чтобы получить лучшую производительность, с перспективой потенциально многопоточности в будущем.

Да, библиотека будет выпущена как с открытым исходным кодом, если кто-то заинтересован в ее использовании.

4b9b3361

Ответ 1

Если вам нужен простой ответ, то нет простого ответа. Никакое количество вызывающих ответов (и подразумеваемых людей) "ленивый" не поможет.

Как быстро я должен ожидать выполнения распределения памяти (MB/s) на стандартном современном процессоре?

На скорости, с которой JVM может обладать нулевой памятью, при условии, что распределение не вызывает сбор мусора. Если он запускает сборку мусора, невозможно предсказать, не зная, какой алгоритм GC используется, размер кучи и другие параметры, а также анализ рабочего набора приложений, не содержащих мусор, в течение всего жизненного цикла приложения.

Как распределяется эффект распределения размера размещения?

См. выше.

Какая точка безубыточности для количества/размера распределений против повторного использования в пуле?

Если вам нужен простой ответ, то нет простого ответа.

Золотое правило: чем больше ваша куча (до объема доступной физической памяти), тем меньше амортизированная стоимость GC'ing объекта мусора. С быстрым копирующим сборщиком мусора амортизированная стоимость освобождения объекта мусора приближается к нулю, когда куча становится больше. Стоимость GC фактически определяется (в упрощенном виде) количеством и размером объектов без мусора, с которыми GC сталкивается.

В предположении, что ваша куча велика, стоимость жизненного цикла выделения и GC'ing большого объекта (в одном цикле GC) приближается к стоимости обнуления памяти при размещении объекта.

EDIT. Если все, что вам нужно, это простые числа, напишите простое приложение, которое выделяет и отбрасывает большие буферы и запускает его на вашей машине с различными параметрами GC и heap и видит, что происходит. Но будьте осторожны, что это не даст вам реалистичного ответа, потому что реальные затраты GC зависят от приложений, которые не являются мусорными объектами.

Я не собираюсь писать для вас ориентир, потому что Я знаю, что он даст вам фиктивные ответы.

EDIT 2: в ответ на комментарии OP.

Итак, я должен ожидать, что распределения будут выполняться так же быстро, как System.arraycopy, или полностью JITed-цикл инициализации массива (около 1 ГБ/с на моей последней скамье, но я сомневаюсь в результате)?

Теоретически да. На практике трудно измерить таким образом, чтобы отделить затраты на распределение от затрат ГХ.

По размеру кучи, вы говорите, что выделение большего объема памяти для использования JVM фактически снижает производительность?

Нет, я говорю, что это может увеличить производительность. Значительно. (При условии, что вы не столкнетесь с эффектами виртуальной памяти на уровне OS.)

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

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

Но если вы намерены пойти по этому пути, создайте интерфейс пула буферов с двумя реализациями. Первый - это реальный поточный буферный пул, который перерабатывает буферы. Второй - это фиктивный пул, который просто выделяет новый буфер каждый раз, когда вызывается alloc, и обрабатывает dispose как no-op. Наконец, разрешите разработчику приложения выбирать между реализациями пула с помощью метода setBufferPool и/или параметров конструктора и/или свойств конфигурации времени выполнения. Приложение также должно быть в состоянии предоставить класс/экземпляр пула буферов собственного изготовления.

Ответ 2

Когда это больше, чем молодое пространство.

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

На моей машине 32kb превосходит молодое пространство. Поэтому было бы целесообразно повторно использовать его.

Ответ 3

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

Ответ 4

Ответ с совершенно другого направления: пусть пользователь вашей библиотеки решит.

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

Итак, создайте механизм объединения в качестве интерфейса и на основе некоторого параметра конфигурации выберите реализацию, используемую вашей библиотекой. Установите значение по умолчанию для того, чтобы ваши тестовые тесты определяли как лучшее решение. 1 И да, если вы используете интерфейс, вам придется полагаться на JVM, достаточно умный для встроенных вызовов. 2


(1) Под "эталоном" подразумевается долгосрочная программа, которая осуществляет вашу библиотеку за пределами профилировщика, передавая ей различные входы. Профилеры чрезвычайно полезны, но так же измеряют общую пропускную способность через час времени настенных часов. На нескольких компьютерах с разным размером кучи и нескольких разных JVM, работающих в одно- и многопоточных режимах.

(2) Это может привести вас к еще одной дискуссии о относительной производительности различных кодов операций invoke.

Ответ 5

Короткий ответ: не буферизировать.

Причины следующие:

  • Не оптимизируйте его, пока он не станет узким местом.
  • Если вы его переработаете, накладные расходы на управление пулом станут еще одним узким местом
  • Постарайтесь доверять JIT. В последней JVM ваш массив может выделяться в STACK, а не HEAP.
  • Поверьте мне, JRE обычно справляется с ними быстрее и лучше, чем вы, сделав сам.
  • Держите его простым, для упрощения чтения и отладки

Когда вы должны перерабатывать объект:

  • только если он тяжелый. Размер памяти не сделает ее тяжелой, но внутренние ресурсы и процессорный цикл делают, что завершает добавление стоимости и цикл процессора.
  • Вы можете захотеть их переработать, если они "ByteBuffer", а не байт []

Ответ 6

Имейте в виду, что эффекты кеша, вероятно, будут больше проблемой, чем стоимость "new int [size]" и соответствующей коллекции. Поэтому повторное использование буферов - хорошая идея, если у вас хорошая временная локальность. Перераспределение буфера вместо повторного использования означает, что каждый раз вы можете получать разные куски памяти. Как уже упоминалось, это особенно верно, когда ваши буферы не подходят в молодое поколение.

Если вы выделяете, но затем не используете весь буфер, он также платит за повторное использование, поскольку вы не тратите время на обнуление памяти, которую вы никогда не используете.

Ответ 7

Более важным, чем размер буфера, является количество выделенных объектов и выделенная общая память.

  • Является ли использование памяти проблемой вообще? Если это небольшое приложение, вам не стоит беспокоиться.

Реальное преимущество от объединения состоит в том, чтобы избежать фрагментации памяти. Накладные расходы на выделение/освобождение памяти малы, но недостатком является то, что если вы многократно выделяли много объектов большого размера, память становится более фрагментированной. Использование пула предотвращает фрагментацию.

Ответ 8

Я забыл, что это управляемая память.

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

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

Вы, вероятно, узнаете, что gc даже не вызван вообще. Поэтому написать код для оптимизации этого будет пустой тратой времени.

с сегодняшним большим объемом памяти я подозреваю, что 90% времени это вообще не стоит делать. Вы не можете определить это на основе параметров - это слишком сложно. Просто профиль - простой и точный.

Ответ 9

Глядя на микро-бенчмарк (код ниже), на моей машине нет заметной разницы во времени, независимо от размера и времени использования массива (я не публикую время, вы можете легко запустить его на своем компьютере: -). Я подозреваю, что это связано с тем, что мусор жив так быстро, что для очистки не так много. Распределение массива должно быть, вероятно, вызовом calloc или malloc/memset. В зависимости от процессора это будет очень быстрая операция. Если массивы выжили в течение более длительного времени, чтобы пройти мимо начальной области GC (питомника), тогда время для того, которое выделило несколько массивов, может занять немного больше времени.

код:

import java.util.Random;

public class Main
{
    public static void main(String[] args) 
    {
        final int size;
        final int times;

        size  = 1024 * 128;
        times = 100;

        // uncomment only one of the ones below for each run
        test(new NewTester(size), times);   
//        test(new ReuseTester(size), times); 
    }

    private static void test(final Tester tester, final int times)
    {
        final long total;

        // warmup
        testIt(tester, 1000);
        total = testIt(tester, times);

        System.out.println("took:   " + total);
    }

    private static long testIt(final Tester tester, final int times)
    {
        long total;

        total = 0;

        for(int i = 0; i < times; i++)
        {
            final long start;
            final long end;
            final int value;

            start = System.nanoTime();
            value = tester.run();
            end   = System.nanoTime();
            total += (end - start);

            // make sure the value is used so the VM cannot optimize too much
            System.out.println(value);
        }

        return (total);
    }
}

interface Tester
{
    int run();
}

abstract class AbstractTester
    implements Tester
{
    protected final Random random;

    {
        random = new Random(0);
    }

    public final int run()
    {
        int value;

        value = 0;

        // make sure the random number generater always has the same work to do
        random.setSeed(0);

        // make sure that we have something to return so the VM cannot optimize the code out of existence.
        value += doRun();

        return (value);
    }

    protected abstract int doRun();
}

class ReuseTester
    extends AbstractTester
{
    private final int[] array;

    ReuseTester(final int size)
    {
        array = new int[size];
    }

    public int doRun()
    {
        final int size;

        // make sure the lookup of the array.length happens once
        size = array.length;

        for(int i = 0; i < size; i++)
        {
            array[i] = random.nextInt();
        }

        return (array[size - 1]);
    }
}

class NewTester
    extends AbstractTester
{
    private int[] array;
    private final int length;

    NewTester(final int size)
    {
        length = size;
    }

    public int doRun()
    {
        final int   size;

        // make sure the lookup of the length happens once
        size = length;
        array = new int[size];

        for(int i = 0; i < size; i++)
        {
            array[i] = random.nextInt();
        }

        return (array[size - 1]);
    }
}

Ответ 10

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

Для вычисления мне нужны 1000 разных матриц размером 1000 x 1000, поэтому он кажется достойным тестом.

Моя система - Ubuntu Linux со следующей виртуальной машиной.

java version "1.7.0_65"
Java(TM) SE Runtime Environment (build 1.7.0_65-b17)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)

Матрицы повторного использования были примерно на 10% медленнее (среднее время работы над 5 исполнением 17354ms против 15708 мс. Я не знаю, будет ли он еще быстрее, если матрица будет намного больше.

Вот соответствующий код:

private void computeSolutionCreatingNewMatrices() {
    computeBaseCase();
    smallest = Integer.MAX_VALUE;
    for (int k = 1; k <= nVertices; k++) {
        current = new int[nVertices + 1][nVertices + 1];
        for (int i = 1; i <= nVertices; i++) {
            for (int j = 1; j <= nVertices; j++) {
                if (previous[i][k] != Integer.MAX_VALUE && previous[k][j] != Integer.MAX_VALUE) {
                    current[i][j] = Math.min(previous[i][j], previous[i][k] + previous[k][j]);
                } else {
                    current[i][j] = previous[i][j];
                }
                smallest = Math.min(smallest, current[i][j]);
            }
        }
        previous = current;
    }
}

private void computeSolutionReusingMatrices() {
    computeBaseCase();
    current = new int[nVertices + 1][nVertices + 1];
    smallest = Integer.MAX_VALUE;
    for (int k = 1; k <= nVertices; k++) {            
        for (int i = 1; i <= nVertices; i++) {
            for (int j = 1; j <= nVertices; j++) {
                if (previous[i][k] != Integer.MAX_VALUE && previous[k][j] != Integer.MAX_VALUE) {
                    current[i][j] = Math.min(previous[i][j], previous[i][k] + previous[k][j]);
                } else {
                    current[i][j] = previous[i][j];
                }
                smallest = Math.min(smallest, current[i][j]);
            }
        }
        matrixCopy(current, previous);
    }
}

private void matrixCopy(int[][] source, int[][] destination) {
    assert source.length == destination.length : "matrix sizes must be the same";
    for (int i = 0; i < source.length; i++) {
        assert source[i].length == destination[i].length : "matrix sizes must be the same";
        System.arraycopy(source[i], 0, destination[i], 0, source[i].length);
    }        
}

Ответ 11

Я думаю, что ответ, который вам нужен, связан с "порядком" (измерение пространства, а не времени!) алгоритма.

Пример файла копии

Например, если вы хотите скопировать файл, который вам нужно прочитать из входного потока и записать в выходной поток. Порядок TIME равен O (n), поскольку время будет пропорционально размеру файла. Но порядок SPACE будет O (1), потому что программа, которую вам нужно будет сделать, будет содержать фиксированный объем памяти (вам понадобится только один фиксированный буфер). В этом случае ясно, что удобно повторно использовать тот самый буфер, который вы создали в начале программы.

Отнести политику буфера к структуре выполнения алгоритма

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

  • попытайтесь исправить размер буферов (даже жертвуя небольшим количеством памяти).
  • Попробуйте посмотреть, какая структура исполнение: например, если вы алгоритм пересекает какое-то дерево и вы буферы связаны с каждый node, возможно, вам нужно только O (log n) буферов... поэтому вы можете сделать образованное предположение о требуемом пространстве.
  • Также, если вам нужны разные буферы, но вы можете организовать разные сегменты одного и того же массив... может быть, это лучше Решение.
  • Когда вы отпускаете буфер, вы можете добавьте его в пул буферов. Что пул может быть кучей, упорядоченным по "подходящие" критерии (буферы, которые наиболее подходящий должен быть первым).

Я пытаюсь сказать: нет никакого фиксированного ответа. Если вы создали нечто, что вы можете повторно использовать... возможно, лучше использовать его повторно. Трудная часть заключается в том, чтобы найти, как вы можете это сделать, не навлекая на себя лишние расходы на управление буфером. Здесь, когда анализ алгоритма пригодится.

Надеюсь, это поможет...:)