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

1

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

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

Я пробовал как обычный, более простой подход, так и один с помощью Regex.

Стандартный подход:

public static boolean isMatch(String terroristName, String clientName) {
        String[] terroristArray = terroristName.split(" ");
        String[] clientArray = clientName.split(" ");

        int size = clientArray.length;
        int ctrl = 0;
        boolean alreadyFound = false;
        for (String client : clientArray) {
            for (String terrorist : terroristArray) {
                //if already found a match, stop comparing with rest of the words from terrorist name
                if (!alreadyFound)
                    if (client.compareTo(terrorist) == 0) {
                        alreadyFound = true;
                        ctrl++;
                        break;
                    }
            }
            alreadyFound = false;
            if (ctrl == 0 && !alreadyFound) {
                //if first word of client is not found in whole terrorist name
                //then exit loop, no match possible
                break;
            } 
        }

        if (ctrl == size)
            return true;
        else
            return false;
    }

Regex подход:

public static boolean isRegexMatch(String terroristName, String clientName) {
        boolean result = false;
        String[] clientNameArray = clientName.split(" ");
        String myPattern = "^";
       //build pattern using client name
        for (String cname : clientNameArray) {
            myPattern += "(?=.*\\b" + cname + "\\b)";
        }
        myPattern += ".*$";

        Pattern pattern = Pattern.compile(myPattern);
        Matcher matcher = pattern.matcher(terroristName);
        // check all occurance
        while (matcher.find()) {
            result = true;
        }
        return result;
    }

Петля, сравнивающая 2 списка имен:

        for (Person terrorist : terrorists) {
            System.setOut(matchPrintStream);
            for (Person client : clients) {
                if (Util.isRegexMatch(terrorist.getNoDuplicatesName(), client.getName())) {
                    System.out.println(client.getId() + ";" + client.getName() + ";" + terrorist.getId() + ";" +
                                       terrorist.getName());
                }
            }
        }

Эти два набора имеют следующие размеры:

  • террорист = aprox 16000
  • клиенты = около 3,4 миллиона

Продолжительность обоих методов довольно медленная:

ps -ef | grep 42810
42810 41919 99 17:47 pts/0    00:52:23 java -Xms1024M -Xmx1024M -classpath ojdbc6.jar:TerroristBuster.jar ro.btrl.mis.mihai.Main

К моменту превышения времени выполнения 00:52:23 он обработал около 170 записей, а это значит, что для завершения потребуется несколько дней. Я знаю, что он имеет большую сложность, не зная, как его снизить. Как вы думаете, может быть, что-то другое, кроме List? Я полагал, что это будет наиболее быстрое использование foreach с момента случайного доступа.

Может ли этот код быть улучшен/изменен каким-либо образом, чтобы улучшить время выполнения, или я просто занимаюсь слишком большим набором данных?

  • 0
    Вы пытались построить пересечение между ними, используя commons-collection4? Не уверен, что это будет работать быстрее, но по сути это то, что вы хотите, верно? Может быть, попробовать.
  • 3
    начните с удаления дублированной работы, вы разделяете одно и то же имя террористов каждый раз, когда оно используется в методе isMatch. Эта проблема также выглядит как параллелизуемая. Предварительно обработайте террористов, а затем разделите клиентов по нескольким потокам, здесь подойдут fork / join или новые параллельные методы Java8.
Показать ещё 8 комментариев
Теги:
set
matching

1 ответ

3
Лучший ответ

Если вы можете использовать Java 8, это должно быть очень легко распараллеливаться.

Во-первых, у вас не так много клиентов, поэтому препроцесс:

final Collection<Collection<String>> processedClients = clients.parallelStream().
        map(c -> c.split("\\s+")).
        map(Arrays::asList).
        collect(toList());

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

Затем нам нужно зациклиться на всех террористах:

terrorists.parallelStream().
        map(t -> t.split("\\s+")).
        map(t -> Stream.of(t).collect(toSet())).
        forEach(t -> {
            processedClients.parallelStream().forEach(c -> {
                if (t.containsAll(c)) {
                    System.out.println("Match found t:" + t + ", c:" + c);
                }
            });
        });

Здесь для каждого террориста мы разделяем их имя, но на этот раз мы превращаем его в Set потому что Set имеет O(1) contains() - это означает, что проверка того, содержится ли целое имя клиента во всем террористическом имени, займет время пропорционально размеру имени клиента.

Затем мы используем forEach для forEach террористов и другого forEach чтобы зацикливаться на клиентах, мы проверяем имя террористов Set containsAll имя клиента.

Опять это происходит параллельно.

Теоретически это не займет много времени. Для хранения обработанных имен клиентов в памяти может потребоваться бит ОЗУ, но это не должно быть слишком много - около 1 ГБ.

РЕДАКТИРОВАТЬ

Вот переписать более раннюю версию (1.7, но если вы удалите нотацию алмаза, она должна работать на 1.5)

Сначала вам нужны два класса обработки, они представлены отдельным рабочим потокам:

final class NameProcessor implements Callable<Collection<String>> {

    private final String name;

    public NameProcessor(final String name) {
        this.name = name;
    }

    @Override
    public Collection<String> call() throws Exception {
        return Arrays.asList(name.split("\\s+"));
    }
}

final class TerroristProcessor implements Runnable {

    private final String name;

    public TerroristProcessor(final String name) {
        this.name = name;
    }

    @Override
    public void run() {
        final Set<String> splitName = new HashSet<>(Arrays.asList(name.split("\\s+")));
        for (final Collection<String> client : proccessedClients) {
            if (splitName.containsAll(client)) {
                System.out.println("Match found t:" + name + ", c:" + client);
            }
        }
    }
}

Теперь вам нужно ExecutorService и ExecutorCompletionService:

final ExecutorService es = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
final ExecutorCompletionService<Collection<String>> cs = new ExecutorCompletionService<>(es);

Теперь вам сначала нужно обработать своих клиентов, как и раньше:

for (final String name : clients) {
    cs.submit(new NameProcessor(name));
}
final Collection<Collection<String>> proccessedClients = new LinkedList<>();
for (int i = 0; i < clients.size(); ++i) {
    try {
        proccessedClients.add(cs.take().get());
    } catch (InterruptedException ex) {
        return;
    } catch (ExecutionException ex) {
        throw new RuntimeException(ex);
    }
}

А затем обработайте террористов:

final Collection<Future<?>> futures = new LinkedList<>();
for (final String terrorist : terrorists) {
    futures.add(es.submit(new TerroristProcessor(terrorist)));
}
es.shutdown();
es.awaitTermination(1, TimeUnit.DAYS);
for (final Future<?> f : futures) {
    try {
        f.get();
    } catch (ExecutionException ex) {
        throw new RuntimeException(ex);
    }
}

Цикл над фьючерсами - проверка ошибок обработки.

РЕДАКТИРОВАТЬ

OP хочет обрабатывать пользовательские объекты, а не коллекции String.

Я бы предположил, что у вас есть какой-то класс Person например:

final class Person {
    private final int id;
    private final String name;

    //constructor

    //getters and setters
}

Затем вы можете просто создать класс-оболочку следующим образом:

final class PersonWrapper {
    private final Person person;
    private final Collection<String> processedName;        

    //constructor

    //getters and setters                
}

И создайте класс результата следующим образом:

final class ProblemClient {
    private final Person client;
    private final Person terrorist;

    //constructor

    //getters and setters  
}

И просто перепишите код соответствующим образом:

final class NameProcessor implements Callable<PersonWrapper> {

    private final Person person;

    public NameProcessor(final Person person) {
        this.person = person;
    }

    @Override
    public PersonWrapper call() throws Exception {
        return new PersonWrapper(person, Arrays.asList(person.getName().split("\\s+")));
    }
}

final ExecutorService es = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
final ExecutorCompletionService<PersonWrapper> cs = new ExecutorCompletionService<>(es);

for (final Person client : clients) {
    cs.submit(new NameProcessor(client));
}
final Collection<PersonWrapper> proccessedClients = new LinkedList<>();
for (int i = 0; i < clients.size(); ++i) {
    try {
        proccessedClients.add(cs.take().get());
    } catch (InterruptedException ex) {
        return;
    } catch (ExecutionException ex) {
        throw new RuntimeException(ex);
    }
}

final class TerroristProcessor implements Runnable {

    private final Person person;
    private final Collection<ProblemClient> results;

    public TerroristProcessor(final Person person, final Collection<ProblemClient> results) {
        this.person = person;
        this.results = results;
    }

    @Override
    public void run() {
        final Set<String> splitName = new HashSet<>(Arrays.asList(person.getName().split("\\s+")));
        for (final PersonWrapper client : proccessedClients) {
            if (splitName.containsAll(client.getProcessedName())) {
                results.add(new ProblemClient(client.getPerson(), person));
            }
        }
    }
}

final Collection<ProblemClient> results = new ConcurrentLinkedQueue<>();
final Collection<Future<?>> futures = new LinkedList<>();
for (final Person terrorist : terrorists) {
    futures.add(es.submit(new TerroristProcessor(terrorist, results)));
}
es.shutdown();
es.awaitTermination(1, TimeUnit.DAYS);
for (final Future<?> f : futures) {
    try {
        f.get();
    } catch (ExecutionException ex) {
        throw new RuntimeException(ex);
    }
}
//process results
for (final ProblemClient problemClient : results) {
    //whatever.
}

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

final class TerroristPreprocessor implements Callable<PersonWrapper> {

    private final Person person;

    public TerroristPreprocessor(final Person person) {
        this.person = person;
    }

    @Override
    public PersonWrapper call() throws Exception {
        final Set<String> splitName = new HashSet<>(Arrays.asList(person.getName().split("\\s+")));
        return new PersonWrapper(person, splitName);
    }
}

final ExecutorService es = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
final ExecutorCompletionService<PersonWrapper> cs = new ExecutorCompletionService<>(es);

for (final Person terrorist : terrorists) {
    cs.submit(new TerroristPreprocessor(terrorist));
}
final Collection<PersonWrapper> proccessedTerrorists = new LinkedList<>();
for (int i = 0; i < terrorists.size(); ++i) {
    try {
        proccessedTerrorists.add(cs.take().get());
    } catch (InterruptedException ex) {
        return;
    } catch (ExecutionException ex) {
        throw new RuntimeException(ex);
    }
}

final class ProblemClientFinder implements Runnable {

    private final Person client;
    private final Collection<ProblemClient> results;

    public ProblemClientFinder(final Person client, final Collection<ProblemClient> results) {
        this.client = client;
        this.results = results;
    }

    @Override
    public void run() {
        final Collection<String> splitName = Arrays.asList(client.getName().split("\\s+"));
        for (final PersonWrapper terrorist : proccessedTerrorists) {
            if (terrorist.getProcessedName().containsAll(splitName)) {
                results.add(new ProblemClient(client, terrorist.getPerson()));
            }
        }
    }
}

final Collection<ProblemClient> results = new ConcurrentLinkedQueue<>();
final Collection<Future<?>> futures = new LinkedList<>();
for (final Person client : clients) {
    futures.add(es.submit(new ProblemClientFinder(client, results)));
}
es.shutdown();
es.awaitTermination(1, TimeUnit.DAYS);
for (final Future<?> f : futures) {
    try {
        f.get();
    } catch (ExecutionException ex) {
        throw new RuntimeException(ex);
    }
}
//process results
for (final ProblemClient problemClient : results) {
    //whatever.
}
  • 0
    К сожалению, я не работаю с Java 8 еще. Постараюсь адаптировать код для более старой версии. Спасибо вам!
  • 1
    @MihaiC Это должно быть довольно легко. Я думаю, что предварительная обработка пользователей в полезные типы данных, вероятно, будет самой большой экономией - начнем с этого.
Показать ещё 9 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню