Оптимизация потоков в Java при 100% загрузке процессора

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

Чтобы максимально быстро выполнить большую часть работы: лучше ли мне просто запускать больше потоков, когда мне нужно выполнить больше работы, и позволить планировщику потоков Java управлять распределением работы, или если бы он стал умнее и управлял рабочей нагрузкой, чтобы поддерживать процессор ниже 100% уведут меня быстрее?

Машина посвящена моему Java-приложению.

EDIT:

Спасибо за фантастический вклад!

Задачи имеют различную сложность и включают в себя операции ввода-вывода, поэтому наличие пула с низким потоком, скажем, 4, может привести к тому, что процессор будет работать только на 20%. У меня нет возможности узнать, сколько задач на самом деле приведет процессор к 100%.

Я подумал, должен ли я контролировать ЦП через RMI и динамически набирать объем работы вверх или вниз, или мне просто все равно, и пусть ОС справится с этим.

 trutheality12 апр. 2012 г., 19:06
@ Расскажите о своем редактировании: если вы можете разделить ваши потоки на потоки с интенсивным использованием процессора и IO, вы можете поместить потоки с интенсивным использованием процессора в пул # -of-core и позволить потокам IO порождаться без пула. Это должно в теории дать вам лучшее использование.
 trutheality12 апр. 2012 г., 04:15
Используйте пул потоков фиксированного размера с таким количеством потоков, сколько у вас есть процессоров. 100% - это не обязательно плохо, но вы не можете сказать оптимальное использование 100% процессорного времени из-за перегруженных ситуаций, таких как отключение, просто глядя на загрузку процессора.
 Martin James12 апр. 2012 г., 11:28
Я думал, что вы используете пул из 100 потоков. Это то, что новый поток создается для каждой задачи? Если так, прекратите делать это сейчас и используйте пул, как предложено @trutheality

Ответы на вопрос(5)

но с минимальным количеством потоков. 100 потоков выглядит слишком много.

 12 апр. 2012 г., 11:12
Зачем? Что не так с 100 потоками (при условии, что ОЗУ достаточно для хранения всех стеков и т. Д. Без постоянной подкачки)?
 12 апр. 2012 г., 14:00
@assilias показал, что 100 потоков не являются существенной проблемой в Java. В C ++ 2000 потоков не является существенной проблемой.

что ваш процессор работает на 100%, мало что говорит о том, насколько они занятыuseful Работа. В вашем случае вы используете больше потоков, чем ядер, поэтому 100% включает некоторое переключение контекста и использует излишнюю память (небольшое влияние на 100 потоков), что является неоптимальным.

Для задач с интенсивным использованием процессора я обычно использую эту идиому:

private final int NUM_THREADS = Runtime.getRuntime().availableProcessors() + 1;
private final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

Использование большего количества потоков, как указали другие, только вносит ненужное переключение контекста.

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

EDIT

Чтобы ответить на комментарий @MartinJames, я провел (упрощенный) бенчмарк - результат показывает, что переход от размера пула = количество процессоров + 1 до 100 лишь незначительно снижает производительность (давайте назовем это 5%) - переходя к повышению цифры (1000 и 10000) действительно сильно влияют на производительность.

Results are the average of 10 runs:
Pool size: 9: 238 ms. //(NUM_CORES+1)
Pool size: 100: 245 ms.
Pool size: 1000: 319 ms.
Pool size: 10000: 2482 ms.

Код:

public class Test {

    private final static int NUM_CORES = Runtime.getRuntime().availableProcessors();
    private static long count;
    private static Runnable r = new Runnable() {

        @Override
        public void run() {
            int count = 0;
            for (int i = 0; i < 100_000; i++) {
                count += i;
            }
            Test.count += count;
        }
    };

    public static void main(String[] args) throws Exception {
        //warmup
        runWith(10);

        //test
        runWith(NUM_CORES + 1);
        runWith(100);
        runWith(1000);
        runWith(10000);
    }

    private static void runWith(int poolSize) throws InterruptedException {
        long average = 0;
        for (int run = 0; run < 10; run++) { //run 10 times and take the average
            Test.count = 0;
            ExecutorService executor = Executors.newFixedThreadPool(poolSize);
            long start = System.nanoTime();
            for (int i = 0; i < 50000; i++) {
                executor.submit(r);
            }
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);
            long end = System.nanoTime();
            average += ((end - start) / 1000000);
            System.gc();
        }
        System.out.println("Pool size: " + poolSize + ": " + average / 10 + " ms.  ");
    }
}
 12 апр. 2012 г., 12:05
спасибо, я должен был добавить «больше потоков, которые не будут существенно увеличивать накладные расходы, пока стеки и т. д. не превысят доступную оперативную память и не начнут выгружаться». Если довести до крайности почти все плохо :)
 12 апр. 2012 г., 11:46
@MartinJames Смотрите мои изменения.
 12 апр. 2012 г., 12:11
@MartinJames Что ты имеешь в виду? Код в моем ответе. Единственное предостережение в том, что при большем количестве потоков GC включается даже между вызовами System.gc ().
 12 апр. 2012 г., 11:14
«Использование большего количества потоков, как указали другие, только приводит к ненужному переключению контекста». Когда количество готовых потоков равно количеству ядер, переключение контекста будет таким же большим, как вы собираетесь получить. Добавление большего количества потоков не приведет к значительному увеличению накладных расходов.
 12 апр. 2012 г., 12:11
может быть, вы могли бы опубликовать свой тестовый код - я бы тоже мог попробовать! Как вы измеряете производительность?
Решение Вопроса

требующих большого объема вычислений, в параллельных потоках, вы очень быстро достигнете точки уменьшения отдачи. Фактически, если имеется N процессоров (ядер), то вам не нужно больше N таких потоков. Теперь, если задачи иногда приостанавливаются для ввода-вывода или взаимодействия с пользователем, то правильное число может быть несколько больше. Но в целом, если в какой-то момент существует больше потоков, которые хотят выполнять вычисления, чем имеется доступных ядер, то ваша программа тратит время на переключение контекста, т. Е. Планирование стоит вам.

 12 апр. 2012 г., 15:56
Более эмпирические результаты. C ++ Процессоемкие задачи. i7, 3GHz, 4 ядра (8 Вт. гипер), 12 ГБ оперативной памяти. ticks / poolThreadCount / taskManagerCPU: 356/8/34, 287/16/29, 280/80/30, 284/800/28. Оптимальное количество потоков в пуле больше, чем [количество ядер], и значительно. Пока что кажется, что если вы хотите, чтобы ваши задачи с интенсивным использованием процессора выполнялись как можно быстрее, используйте 80 потоков. Если вы хотите, чтобы они работали максимально эффективно, используйте 800. Даже я нахожу это необоснованным, поэтому кто-то докажет, что я неправ ...
 12 апр. 2012 г., 18:45
Это интересная проблема, хотя. В течение многих лет я видел сообщения только о создании [нет. из ядер] потоков для лучшей производительности из-за «издержек переключения контекста». Теперь я обнаружил, что потоков намного больше, чем лучше, даже с заданиями, связанными с процессором.
 12 апр. 2012 г., 13:26
@MartinJames - Я не думаю, что выбор пула потоков оптимального размера - это микроуправление. В любом случае: цитируйте, пожалуйста. Ваши заявления бросают вызов каждому стандартному тексту и ссылке. Просто один пример, который согласен со мной:ibm.com/developerworks/library/j-jtp0730/index.html.
 12 апр. 2012 г., 17:35
@ Серый - и так это доказано. Мои задачи скучны и просто выполняют обычные задачи - увеличивают целочисленный член. Я добавил еще один "0" для цикла. Теперь он насчитывает до 100000000, и я ставлю в очередь 400 из них. Мое использование процессора во время теста теперь составляет 100% на всех ядрах 4 / HT8. Ticks / threadCount на данный момент: 21922/8, 20424/80, 20191/800. Лучше с 800 нитями! Вентилятор процессора издает много шума, и здесь он нагревается.
 12 апр. 2012 г., 10:49
Сколько это стоит? Если задач, нагруженных ЦП, много, блок перегружен, а готовых потоков всегда больше, чем процессоров, поэтому вполне вероятно, что ОС прибегнет к изменению набора готовых потоков при каждом прерывании imer. Таким образом, независимо от того, сколько потоков связано с процессором, количество переключателей контекста будет ограничено одним каждые .. 30 мс или что-то еще. Это не важно. То, что вы говорите выше, верно, но не является оправданием для дополнительного управления микропроцессорами пользователем (что очень часто идет не так).

чтобы поддерживать процессор ниже 100%, помогло бы мне быстрее?"

Возможно нет.

Как уже сообщали другие, 100 потоков - это слишком много для пула потоков, если большинство задач требует интенсивной работы ЦП. Это не будет иметь большого значения для производительности на типичных системах - с такой большой перегрузкой будет плохо с 4 потоками и плохо с 400.

Как вы решили на 100 потоков? Почему не 16, скажем?

«Количество потоков не является массовым, скажем, до 100». - это меняется? Просто создайте 16 при запуске и перестаньте управлять ими - просто передайте им очередь и забудьте о них.

Ужасная мысль - вы не создаете новую ветку для каждой задачи?

 12 апр. 2012 г., 12:01
num_processor * 1.5 - отлично. num_processor * 15, не сильно отличается, правда. Да - получите количество процессоров из sysinfo, удвойте его и добавьте число, которое вы впервые подумали о & lt; g & gt;
 12 апр. 2012 г., 11:53
Почему 16? Динамическое регулирование числа потоков в соответствии с количеством доступных процессоров имеет смысл - например, почему бы не (num_processor * 1.5)?

am I best off to just launch more threads when I need to do more work and let the Java thread scheduler handle distributing the work, or would getting smarter and managing the work load to keep the CPU below 100% get me further faster?

По мере добавления все большего количества потоков увеличиваются накладные расходы, связанные с переключением контекста, очисткой кэша памяти, переполнением кэша памяти и управлением потоками ядра и JVM. По мере того, как ваши потоки нагружают процессор своим кером, приоритеты nel снижаются до некоторого минимума, и они достигают минимума временных интервалов. По мере того, как все больше потоков заполняют память, они переполняют различные кеши внутренней памяти ЦП. Существует более высокая вероятность того, что ЦПУ потребуется поменять задание с более медленной памяти. Внутри JVM существует больше локальных конфликтов мьютекса и, возможно, некоторые (может быть, небольшие) дополнительные издержки GC для каждого потока и пропускной способности объекта. В зависимости от того, насколько синхронизированы ваши пользовательские задачи, большее количество потоков приведет к увеличению сброса памяти и конфликту блокировок.

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

Я бы порекомендовал использоватьExecutors.newFixedThreadPool(SOME_NUMBER); и представить вам работу к нему. Затем вы можете выполнить несколько запусков, изменяя количество потоков вверх и вниз, пока не найдете оптимальное количество одновременно работающих пулов в соответствии с работой и архитектурой блока.

Однако следует понимать, что оптимальное количество потоков будет зависеть от количества процессоров и других факторов, которые могут быть нетривиальными для определения. Может потребоваться больше потоков, если они блокируют дисковые или сетевые ресурсы ввода-вывода. Меньше потоков, если работа, которую они выполняют, в основном основана на ЦП.

 12 апр. 2012 г., 17:13
Да, и хотя задания, связанные с процессором, не выполняют системные вызовы, они могут иметь дело с барьерами или блокировками памяти, что также вызывает прерывания JVM.
 12 апр. 2012 г., 17:09
В случае этих связанных с процессором заданий, в которых блок перегружен гораздо большим количеством перетаскиваемых потоков, чем ядер, при каждом изменении работающего набора будут накладные расходы, как вы описали. Это может произойти только при аппаратных прерываниях или системных вызовах. Задачи, интенсивно загружающие процессор, обычно не выполняют частые системные вызовы, поэтому остаются прерывания. Если мы пренебрегаем ошибками страниц, задачи, связанные с процессором, также не будут выполнять много операций ввода-вывода, так что останется прерывание таймера. Частота этого не зависит от количества готовых потоков, поэтому генерируемые накладные расходы не зависят от количества готовых потоков.
 12 апр. 2012 г., 17:12
Я согласен, хотя окно временного интервала не является фиксированным. Он падает до некоторого уровня, так как ядро наказывает связанные с CPU задания. Кроме того, я не уверен на 100%, что ошибками страницы можно пренебречь. Я не уверен, что у процессора / ядра есть возможность использовать расположение памяти при планировании в эти дни - вероятно, нет.
 12 апр. 2012 г., 10:54
А? Переключение контекста происходит только тогда, когда ОС вводится по прерыванию. Если имеется большой набор готовых к загрузке потоков, интенсивно использующих процессор, ОС будет переключаться между запущенным набором (вероятно, в основном после прерывания по таймеру). Как только набор готовых потоков становится больше, чем количество ядер, издержки переключения контекста становятся почти постоянными по мере добавления большего количества потоков.
 12 апр. 2012 г., 16:29
Для меня переключение контекста - это то, что происходит на уровне ЦП, когда поток выполняет IO или срабатывает таймер, и в очереди выполнения слишком много заданий. Это сбрасывает кэш-память (L1), копирует состояние выполнения в память, переходит в следующее задание. Я согласен с тем, что существует уровень служебных данных JVM / OS, но он более сложен с учетом переполнения кэш-памяти, штрафов за приоритет процессора и затрат JVM. Но я должен в своем ответе больше говорить о пределах.

Ваш ответ на вопрос