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

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

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

Править:

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

EDIT2:

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

Предложения для получения хорошего сравнительного теста:

  1. Нагрейте код прежде, чем выполнить его в сравнительном тесте. Методы нужно все назвать по крайней мере 1 000 10000 раз для получения полной оптимизации JIT.
  2. Удостоверьтесь сравниваемые методы, выполненные по крайней мере для 1 10 секунд, и используйте System.nanotime, если это возможно, для получения точных синхронизаций.
  3. Выполненный сравнительный тест в системе, которая только запускает минимальные приложения
  4. Выполненный сравнительный тест 3-5 раз и все случаи отчета, таким образом, мы видим, насколько последовательный это.

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

Что я знаю (и не нуждаются повторенный):

  • Выделение памяти Java и GC быстры и добираются быстрее.
  • Пулинг объектов раньше был хорошей оптимизацией, но теперь он повреждает производительность большую часть времени.
  • Пулинг объектов является "не обычно хорошей идеей, если объекты не являются дорогими для создания". Yadda yadda.

Что я не знаю:

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

Маршруты к ПРИНЯТОМУ ответу (больше лучше):

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

Контекст:

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

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

31
задан Lawrence Dol 20 February 2010 в 08:45
поделиться

10 ответов

Если вам нужен простой ответ, то простого ответа не существует. Никакие «ленивые» ответы не помогут.

Насколько быстро будет выполняться выделение памяти (МБ / с) на стандартном современном процессоре?

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

Как размер распределения влияет на скорость распределения?

См. Выше.

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

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

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

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

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

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

РЕДАКТИРОВАТЬ 2 : В ответ на комментарии OP.

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

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

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

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

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

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

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

26
ответ дан 27 November 2019 в 22:23
поделиться

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

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

1
ответ дан 27 November 2019 в 22:23
поделиться

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

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

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

0
ответ дан 27 November 2019 в 22:23
поделиться

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

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

На моей машине 32 кб превышает молодое пространство. Так что имеет смысл использовать его повторно.

13
ответ дан 27 November 2019 в 22:23
поделиться

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

3
ответ дан 27 November 2019 в 22:23
поделиться

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

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

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

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

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

1
ответ дан 27 November 2019 в 22:23
поделиться

Короткий ответ: Не буферизируйте.

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

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

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

  • только если он тяжелый. Размер памяти не сделает его тяжелым, но это делают родные ресурсы и цикл процессора, которые завершают добавление стоимости и цикл процессора.
  • Вы можете захотеть их переработать, если это "ByteBuffer", а не байт[]
2
ответ дан 27 November 2019 в 22:23
поделиться

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

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

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


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

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

3
ответ дан 27 November 2019 в 22:23
поделиться

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

Скопируйте пример файла

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

Свяжите политику буферов со структурой исполнения вашего алгоритма

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

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

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

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

0
ответ дан 27 November 2019 в 22:23
поделиться

Глядя на микротест (код ниже), на моей машине нет заметной разницы во времени независимо от размера и времени использования массива (я не публикую время, вы можете легко запустить на своей машине :-). Я подозреваю, что это потому, что мусор жив в течение столь короткого времени, что нечем заняться. Выделение массива, вероятно, должно быть вызвано вызовом 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]);
    }
}
1
ответ дан 27 November 2019 в 22:23
поделиться
Другие вопросы по тегам:

Похожие вопросы: