Оптимизация кода для преобразования скрытого списка целых чисел в список объектов в Java

#java #optimization

#java #оптимизация

Вопрос:

У меня есть от 50000 до 500000 идентификаторов сотрудников, и я хочу, чтобы эти идентификаторы сотрудников преобразовывались в подробные объекты.

Я сделал что-то подобное, чтобы добиться этого:

 private Set<Detail> setDetail(List<Integer> employees, Group group) {
  Set<Detail> details = employees.stream().parallel().map(id -> new Detail(id, group)).collect(Collectors.toSet());
  return details ;
}
  

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

Комментарии:

1. parallel является хорошим выбором в случае большого количества данных.

2. Я пробовал это с / без параллели, но не заметил такой производительности. Разве я не могу лучше улучшить это?

3. Можете ли вы определить slow? объекты 500k не должны быть проблемой на разумном оборудовании, является ли ваше оборудование разумным? Происходит ли что-нибудь «интересное» в Detail конструкторе,?

4. Используйте классический пул потоков (ExecutorService) и используйте ConcurrentSkipListSet вместо list .

5. В зависимости от динамики вашего возвращаемого типа, вы можете попробовать ArrayList вместо set . Для меня это было примерно в 5 раз быстрее. Особенно, если вы предварительно выделяете список.

Ответ №1:

Вы должны стараться избегать создания такого количества объектов. Независимо от того, какой алгоритм вы выберете, если ваша БД будет продолжать расти в какой-то момент, вы не сможете разместить все это в памяти. Также узким местом, вероятно, будет получение данных из БД (а не создание объектов).

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

Если после тщательного рассмотрения вы решите, что вам действительно нужно работать с таким количеством объектов, лучшим вариантом будет продолжать работать с примитивами:

 class EmployeesInGroup {
   private final int[] ids;
   private final Group group;
   ...

   Detail get(int idx) {
      return new Details(ids[idx], group);
   }

   int size() {
     return ids.length;
   }
}
  

Затем вы можете перебирать этот список и работать с 1 объектом за раз, не сохраняя их много в памяти:

 EmployeesInGroup list = new EmployeesInGroup(ids, group);
for(int i = 0; i < list.size(); i  ) {
  Detail d = list.get(i);
  ...
}
  

Вы можете реализовать его Iterable и использовать для каждого цикла.

Тесты

Подход, который я перечислил выше, как минимум в 20 раз быстрее, чем создание массива объектов Detail. Работа с потоками и списками еще больше замедляет процесс. Я не проверял, Integer но я бы предсказал, что это замедлит все еще в 2 раза или что-то в этом роде.

 Benchmark                                        Mode  Cnt     Score    Error  Units
EmployeeConversionBenchmark.objectArray         thrpt   20   368.702 ±  3.483  ops/s
EmployeeConversionBenchmark.primitiveArray      thrpt   20  7595.080 ± 68.841  ops/s
EmployeeConversionBenchmark.streamsWithObjects  thrpt   20   197.923 ±  1.616  ops/s
  

Вот код, который я использовал:

 import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;

import java.util.Arrays;
import java.util.List;
import java.util.Random;

import static java.util.stream.Collectors.toList;

public class EmployeeConversionBenchmark {
    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(new String[]{EmployeeConversionBenchmark.class.getSimpleName()});
    }

    @Benchmark @Fork(value = 1, warmups = 0)
    public int primitiveArray(Data data) {
        EmployeesInGroup e = new EmployeesInGroup(data.ids, data.group);
        int sum = 0;
        for (int i = 0; i < e.size(); i  )
            sum  = e.get(i).getId();
        return sum;
    }
    @Benchmark @Fork(value = 1, warmups = 0)
    public int objectArray(Data data) {
        EmployeesInGroup.Detail[] e = new EmployeesInGroup.Detail[data.ids.length];
        for (int i = 0; i < data.ids.length; i  )
            e[i] = new EmployeesInGroup.Detail(data.ids[i], data.group);

        int sum = 0;
        for (EmployeesInGroup.Detail detail : e)
            sum  = detail.getId();
        return sum;
    }
    @Benchmark @Fork(value = 1, warmups = 0)
    public int streamsWithObjects(Data data) {
        List<EmployeesInGroup.Detail> e = Arrays.stream(data.ids).mapToObj(id -> new EmployeesInGroup.Detail(id, data.group)).collect(toList());
        int sum = 0;
        for (EmployeesInGroup.Detail detail : e)
            sum  = detail.getId();
        return sum;
    }

    @State(Scope.Benchmark)
    public static class Data {
        private final int[] ids = new int[500_000];
        private final EmployeesInGroup.Group group = new EmployeesInGroup.Group();
        public Data() {
            for (int i = 0; i < ids.length; i  )
                ids[i] = new Random().nextInt();
        }
    }

    public static class EmployeesInGroup {
        private final int[] ids;
        private final Group group;

        public EmployeesInGroup(int[] ids, Group group) {
            this.ids = ids;
            this.group = group;
        }
        public Detail get(int idx) {
            return new Detail(ids[idx], group);
        }

        public int size() {
            return ids.length;
        }

        public static class Group {
        }

        public static class Detail {
            private final int id;
            private final Group group;

            public Detail(int id, Group group) {
                this.id = id;
                this.group = group;
            }
            public int getId() {
                return id;
            }
        }
    }
}
  

Комментарии:

1. «без сохранения большого их количества в памяти» — после вызова: new EmployeesInGroup(идентификаторы, группа) , если не в памяти, где они хранятся?

2. @DavidSoroko, EmployeesInGroup содержит примитивный int[] массив, а не список всех Detail объектов. С которым намного быстрее работать. При повторении он будет создавать 1 недолговечный Detail объект за раз. Затем вы перейдете к следующему и забудете ссылку на предыдущий объект. Это позволит GC очистить его. Таким образом, только массив int[] будет храниться в памяти в течение длительного времени.

3. Создание и «забывание» (если забывание имеет смысл для приложения) одного объекта за раз требует меньше памяти, чем выделение всех экземпляров за один раз, а затем их удаление за один раз. Но считаете ли вы, что ваш подход будет быстрее? Если вы считаете, что это так, не могли бы вы сравнить и сравнить?

4. @DavidSoroko, конечно. Мои измерения показывают, что по крайней мере в 20 раз ускоряется, просто сохраняя int[] вместо Details[] . Потоки и целое число сделают это еще медленнее. Я скоро опубликую тесты.

5. Спасибо, что нашли время для этого. Я играл с вашим бенчмарком JMH и, чего бы это ни стоило, не думаю, что лучшая производительность primitiveArray связана с GC, поскольку она также имеет крошечные размеры. Скорее всего, это связано с тем, что целые числа расположены в непрерывном фрагменте памяти, в то время как список объектов находится по всей куче.

Ответ №2:

В общем, разговор о производительности и оптимизации должен включать цифры, например: латентность, которую вы пытаетесь достичь, латентность, которую вы фактически наблюдаете, некоторые подробности о вашем оборудовании и т. Д…

Ваша проблема (если она есть) не в опубликованном вами методе. На моем ноутбуке приведенный ниже код выполняется за ~ 250 мс. и даже быстрее (~ 200 мс), если вы удалите parallel() :

 class Detail {
    private Integer id;
    private String group;

    public Detail(Integer id, String group) {
        this.id = id;
        this.group = group;
    }
}

public class Main {
    public Set<Detail> setDetail(List<Integer> employees, String group) {
        return employees.stream().parallel().map(id -> new Detail(id, group)).collect(toSet());
    }

    public static void main(String[] args) {
        List<Integer> idsList = new Random().ints().boxed().limit(500_000).collect(toList());

        long start = System.currentTimeMillis();

        Set<Detail> details = new Main().setDetail(idsList, "group");

        long duration = (System.currentTimeMillis() - start);
        System.out.println("Done in "   duration   " ms. Size was "   details.size());
    }
}
  

Комментарии:

1. Удаление параллели также выполнялось быстрее в моем тесте.

2. Вы могли бы быть быстрее со скучным циклом for.

3. Возможно, вы правы, но это не мой код, который нужно менять. Кроме того, должна быть причина, по которой код выполняется быстрее — за мои деньги код OP достаточно быстр