ExecutorService замедляет многопоточную производительность

Я пытаюсь выполнить простой расчет (он вызывает Math.random() 10000000 раз). Удивительно, но запуск его в простом методе выполняется намного быстрее, чем при использовании ExecutorService.

Я прочитал еще один поток в удивительной точке безубыточности ExecutorService - -- эмпирические правила? и попытался следовать ответу, выполнив Callable с использованием пакетов, но производительность по-прежнему низкая

Как повысить производительность на основе моего текущего кода?

import java.util.*;
import java.util.concurrent.*;

public class MainTest {
    public static void main(String[]args) throws Exception {
        new MainTest().start();;
    }

    final List<Worker> workermulti = new ArrayList<Worker>();
    final List<Worker> workersingle = new ArrayList<Worker>();
    final int count=10000000;

    public void start() throws Exception {
        int n=2;

        workersingle.add(new Worker(1));
        for (int i=0;i<n;i++) {
            // worker will only do count/n job
            workermulti.add(new Worker(n));
        }

        ExecutorService serviceSingle = Executors.newSingleThreadExecutor();
        ExecutorService serviceMulti = Executors.newFixedThreadPool(n);
        long s,e;
        int tests=10;
        List<Long> simple = new ArrayList<Long>();
        List<Long> single = new ArrayList<Long>();
        List<Long> multi = new ArrayList<Long>();

        for (int i=0;i<tests;i++) {
            // simple
            s = System.currentTimeMillis();
            simple();
            e = System.currentTimeMillis();
            simple.add(e-s);

            // single thread
            s = System.currentTimeMillis();
               serviceSingle.invokeAll(workersingle); // single thread
            e = System.currentTimeMillis();
            single.add(e-s);

            // multi thread
            s = System.currentTimeMillis();
               serviceMulti.invokeAll(workermulti);
            e = System.currentTimeMillis();
            multi.add(e-s);
        }
        long avgSimple=sum(simple)/tests;
        long avgSingle=sum(single)/tests;
        long avgMulti=sum(multi)/tests;
        System.out.println("Average simple: "+avgSimple+" ms");
        System.out.println("Average single thread: "+avgSingle+" ms");
        System.out.println("Average multi thread: "+avgMulti+" ms");

        serviceSingle.shutdown();
        serviceMulti.shutdown();
    }

    long sum(List<Long> list) {
        long sum=0;
        for (long l : list) {
            sum+=l;
        }
        return sum;
    }

    private void simple() {
        for (int i=0;i<count;i++){
            Math.random();
        }
    }

    class Worker implements Callable<Void> {
        int n;

        public Worker(int n) {
            this.n=n;
        }

        @Override
        public Void call() throws Exception {
            // divide count with n to perform batch execution
            for (int i=0;i<(count/n);i++) {
                Math.random();
            }
            return null;
        }
    }
}

Вывод для этого кода

Average simple: 920 ms
Average single thread: 1034 ms
Average multi thread: 1393 ms

РЕДАКТИРОВАТЬ: производительность снижается из-за того, что Math.random() является синхронизированным методом.. после изменения Math.random() новым объектом Random для каждого потока производительность улучшилась

Вывод для нового кода (после замены Math.random() на Random для каждого потока)

Average simple: 928 ms
Average single thread: 1046 ms
Average multi thread: 642 ms

person GantengX    schedule 23.08.2011    source источник


Ответы (3)


Math.random() синхронизируется. Весь смысл синхронизации заключается в замедлении процессов, чтобы они не сталкивались. Используйте что-то, что не синхронизировано, и/или дайте каждому потоку свой собственный объект для работы, например новый Случайный.

person Ryan Stewart    schedule 23.08.2011
comment
Ах ты прав! Я не знал, что Math.random() синхронизирован. Как только я добавил новый случайный объект для каждого рабочего, производительность значительно улучшилась. - person GantengX; 23.08.2011
comment
Просто быстрый вопрос: если я попытаюсь поделиться случайным объектом, производительность все равно пострадает. Вы знаете, почему это так? Random.nextDouble не синхронизируется и вызывает Random.next(int), который, в свою очередь, вызывает AtomicLong.compareAndSet.. Я не понимаю, почему это повлияет на производительность. - person GantengX; 23.08.2011
comment
Я предполагаю, что вы только что вернулись к тому, что несколько потоков снова соревнуются за один и тот же ресурс: в данном случае AtomicLong. Только один поток может обновлять свое значение за раз, и оно обновляется дважды при каждом вызове nextDouble(). - person Ryan Stewart; 23.08.2011

Вы бы хорошо прочитали содержимое другой ветки. Там много хороших советов.

Возможно, самая серьезная проблема с вашим тестом заключается в том, что в соответствии с контрактом Math.random(): «Этот метод правильно синхронизирован, чтобы обеспечить правильное использование более чем одним потоком. Однако, если многим потокам необходимо генерировать псевдослучайные числа с большой скоростью , это может уменьшить конкуренцию за каждый поток, чтобы иметь свой собственный генератор псевдослучайных чисел"

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

person Steven Schlansker    schedule 23.08.2011

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

В этом случае самым простым обходным решением является использование отдельного Random в каждом потоке. Проблема заключается в том, что в качестве микротеста ваш цикл на самом деле ничего не делает, а JIT очень хорошо отбрасывает код, который ничего не делает. Обходной путь для этого состоит в том, чтобы суммировать случайные результаты и возвращать их из call(), поскольку этого обычно достаточно, чтобы JIT не отбрасывал код.

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

person Peter Lawrey    schedule 23.08.2011