Удивительная точка безубыточности производительности ExecutorService — эмпирические правила?

Я говорю Grails, так как существует так много библиотек Java. Но я немного пристрастен из-за того, что я родом из Java.

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

18
задан Josh Lee 19 December 2011 в 22:13
поделиться

4 ответа

  1. Использование исполнителей связано с использованием ЦП и / или ядер ЦП, поэтому, если вы создаете пул потоков, который в лучшем случае использует количество ЦП, у вас должно быть столько же потоков, сколько ЦП / ядер.
  2. Вы правы, создание новых объектов стоит слишком дорого. Таким образом, один из способов сократить расходы - использовать партии. Если вы знаете, какой вид и объем вычислений необходимо выполнить, вы создаете пакеты. Так что подумайте о тысячах вычислений, выполненных за одну выполненную задачу. Вы создаете пакеты для каждого потока. Как только вычисление будет выполнено (java.util.concurrent.Future), вы создадите следующий пакет. Даже создание новых пакетов может быть выполнено в parralel (4 процессора -> 3 потока для вычислений, 1 поток для пакетной подготовки). В конце концов, вы можете получить большую пропускную способность, но с более высокими требованиями к памяти (пакеты, инициализация).

Изменить: Я изменил ваш пример и позволил ему работать на моем маленьком двухъядерном ноутбуке x200.

provisioned 2 batches to be executed
simpleCompuation:14
computationWithObjCreation:17
computationWithObjCreationAndExecutors:9

Как вы видите в исходном коде, я также исключил из измерения пакетное обеспечение и жизненный цикл исполнителя. Это более справедливо по сравнению с двумя другими методами.

Посмотрите результаты сами ...

import java.util.List;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecServicePerformance {

    private static int count = 100000;

    public static void main( String[] args ) throws InterruptedException {

        final int cpus = Runtime.getRuntime().availableProcessors();

        final ExecutorService es = Executors.newFixedThreadPool( cpus );

        final Vector< Batch > batches = new Vector< Batch >( cpus );

        final int batchComputations = count / cpus;

        for ( int i = 0; i < cpus; i++ ) {
            batches.add( new Batch( batchComputations ) );
        }

        System.out.println( "provisioned " + cpus + " batches to be executed" );

        // warmup
        simpleCompuation();
        computationWithObjCreation();
        computationWithObjCreationAndExecutors( es, batches );

        long start = System.currentTimeMillis();
        simpleCompuation();
        long stop = System.currentTimeMillis();
        System.out.println( "simpleCompuation:" + ( stop - start ) );

        start = System.currentTimeMillis();
        computationWithObjCreation();
        stop = System.currentTimeMillis();
        System.out.println( "computationWithObjCreation:" + ( stop - start ) );

        // Executor

        start = System.currentTimeMillis();
        computationWithObjCreationAndExecutors( es, batches );    
        es.shutdown();
        es.awaitTermination( 10, TimeUnit.SECONDS );
        // Note: Executor#shutdown() and Executor#awaitTermination() requires
        // some extra time. But the result should still be clear.
        stop = System.currentTimeMillis();
        System.out.println( "computationWithObjCreationAndExecutors:"
                + ( stop - start ) );
    }

    private static void computationWithObjCreation() {

        for ( int i = 0; i < count; i++ ) {
            new Runnable() {

                @Override
                public void run() {

                    double x = Math.random() * Math.random();
                }

            }.run();
        }

    }

    private static void simpleCompuation() {

        for ( int i = 0; i < count; i++ ) {
            double x = Math.random() * Math.random();
        }

    }

    private static void computationWithObjCreationAndExecutors(
            ExecutorService es, List< Batch > batches )
            throws InterruptedException {

        for ( Batch batch : batches ) {
            es.submit( batch );
        }

    }

    private static class Batch implements Runnable {

        private final int computations;

        public Batch( final int computations ) {

            this.computations = computations;
        }

        @Override
        public void run() {

            int countdown = computations;
            while ( countdown-- > -1 ) {
                double x = Math.random() * Math.random();
            }
        }
    }
}
19
ответ дан 30 November 2019 в 07:08
поделиться

Это несправедливый тест для пула потоков по следующим причинам:

  1. Вы вообще не пользуетесь пулом, потому что у вас только 1 поток.
  2. Это задание слишком просто, чтобы накладные расходы на объединение не могли быть оправданы. Умножение на CPU с FPP занимает всего несколько циклов.

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

  1. Поместить задание в очередь
  2. Удалить задание из очереди
  3. Получить поток из пула и выполнить задание
  4. Вернуть поток в пул

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

7
ответ дан 30 November 2019 в 07:08
поделиться

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

Задача очень мала, хотя я не совсем уверен, насколько она мала. Так что количество раз быстрее бессмысленно. В многопоточных ситуациях он будет касаться одних и тех же изменчивых мест, поэтому потоки могут вызвать действительно плохую производительность (используйте экземпляр Random для каждого потока). Также цикл в 47 миллисекунд - это немного мало.

Конечно, переход к другому потоку для выполнения крошечной операции не будет быстрым. Если возможно, разделите задачи на более крупные. JDK7 выглядит так, как будто у него будет структура fork-join, которая пытается поддерживать мелкие задачи из алгоритмов разделения и владения, предпочитая выполнять задачи в одном потоке по порядку, а более крупные задачи вытягиваются из незанятых потоков.

0
ответ дан 30 November 2019 в 07:08
поделиться

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

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

Но я не думаю, что вы полностью уловили суть - исполнители предназначены не для эффективности, они ' re там, чтобы упростить координацию и передачу работы пулу потоков. Они всегда будут менее эффективны, чем простой вызов Runnable. run () самостоятельно (так как в конце дня служба-исполнитель все еще должна это сделать, предварительно выполнив некоторую дополнительную уборку). Они действительно сияют, когда вы используете их из нескольких потоков, требующих асинхронной обработки.

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

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

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

веб-приложение), то исполнители великолепны.

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

веб-приложение), то исполнители великолепны.

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

4
ответ дан 30 November 2019 в 07:08
поделиться
Другие вопросы по тегам:

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