Слишком низкое использование ЦП многопоточного Java-приложения в Windows

18

Я работаю над приложением Java для решения класса задач численной оптимизации - точнее, крупномасштабных задач линейного программирования. Отдельную проблему можно разбить на более мелкие подзадачи, которые можно решать параллельно. Поскольку существует больше подзадач, чем ядер ЦП, я использую ExecutorService и определяю каждую подзадачу как Callable, который передается в ExecutorService. Решение подзадачи требует вызова собственной библиотеки - в данном случае решателя линейного программирования.

проблема

Я могу запустить приложение в Unix и в системах Windows с до 44 физическими ядрами и до 256 г памяти, но время вычислений в Windows на порядок выше, чем в Linux для больших проблем. Windows не только требует значительно больше памяти, но и загрузка ЦП со временем падает с 25% в начале до 5% через несколько часов. Вот скриншот диспетчера задач в Windows:

Загрузка ЦП диспетчера задач

наблюдения

  • Время решения для больших случаев общей проблемы варьируется от часов до дней и занимает до 32 г памяти (в Unix). Время решения для подзадачи находится в диапазоне мс.
  • Я не сталкиваюсь с этой проблемой в отношении небольших проблем, решение которых занимает всего несколько минут.
  • Linux использует оба сокета «из коробки», тогда как Windows требует, чтобы я явно активировал чередование памяти в BIOS, чтобы приложение использовало оба ядра. Независимо от того, что я делаю, это не влияет на ухудшение общего использования ЦП с течением времени.
  • Когда я смотрю на потоки в VisualVM, все потоки пула работают, ни один не находится в ожидании или иначе.
  • Согласно VisualVM, 90% процессорного времени тратится на вызов собственной функции (решение небольшой линейной программы)
  • Сборка мусора не является проблемой, поскольку приложение не создает и не удаляет ссылки на многие объекты. Кроме того, большая часть памяти выделяется вне кучи. 4 г кучи достаточно для Linux и 8 г для Windows для самого большого экземпляра.

Что я пробовал

  • всевозможные аргументы JVM, высокий уровень XMS, высокий уровень метапространства, флаг UseNUMA, другие GC.
  • разные JVM (Hotspot 8, 9, 10, 11).
  • различные нативные библиотеки различных решателей линейного программирования (CLP, Xpress, Cplex, Gurobi).

Вопросов

  • Что обусловливает разницу в производительности между Linux и Windows большого многопоточного Java-приложения, которое интенсивно использует собственные вызовы?
  • Есть ли что-то, что я могу изменить в реализации, что могло бы помочь Windows, например, если бы я избегал использования ExecutorService, который получает тысячи Callables и делал что вместо этого?
Nils
источник
Вы пробовали ForkJoinPoolвместо ExecutorService? 25% загрузка ЦП действительно низкая, если ваша проблема связана с ЦП.
Кароль Доубеки
1
Ваша проблема звучит как то, что должно поднять процессор до 100%, и все же вы на 25%. Для некоторых задач ForkJoinPoolэто более эффективно, чем ручное планирование.
Кароль Доубеки
2
Перебирая версии Hotspot, вы убедились, что используете «серверную», а не «клиентскую» версию? Какова ваша загрузка ЦП в Linux? Кроме того, время работы Windows в течение нескольких дней впечатляет! В чем твой секрет? : P
erickson
3
Может быть, попробуйте использовать Xperf для создания FlameGraph . Это может дать вам некоторое представление о том, что делает процессор (надеюсь, как в режиме пользователя, так и в режиме ядра), но я никогда не делал этого в Windows.
Кароль Доубеки
1
@Nils, оба прогона (unix / win) используют один и тот же интерфейс для вызова нативной библиотеки? Я спрашиваю, потому что это выглядит иначе. Как: Win использует JNA, Linux JNI.
СР

Ответы:

2

Для Windows количество потоков на процесс ограничено адресным пространством процесса (см. Также Марк Руссинович - Расширение границ Windows: процессы и потоки ). Думаю, это вызывает побочные эффекты, когда приближается к пределам (замедление переключения контекста, фрагментация ...). Для Windows я бы попытался разделить рабочую нагрузку на набор процессов. Для аналогичной проблемы, с которой я столкнулся много лет назад, я реализовал библиотеку Java, чтобы сделать это более удобным (Java 8), если хотите, посмотрите: Библиотека для порождения задач во внешнем процессе .

гери
источник
Это выглядит очень интересно! Я немного не решаюсь заходить так далеко (пока) по двум причинам: 1) из-за увеличения производительности при сериализации и отправке объектов через сокеты; 2) если я хочу сериализовать все, что включает в себя все зависимости, которые связаны в задаче - было бы немного работы переписать код - тем не менее, спасибо за полезные ссылки.
Нильс
Я полностью разделяю ваши опасения, и переделка кода потребует некоторых усилий. При обходе графика вам нужно будет ввести пороговое значение для числа потоков, когда пришло время разделить работу на новый подпроцесс. Для решения 2) взгляните на файл сопоставления памяти Java (java.nio.MappedByteBuffer), с помощью которого вы сможете эффективно обмениваться данными между процессами, например данными вашего графика. Godspeed :)
гери
0

Похоже, что Windows кеширует некоторую память в файл подкачки, после того как некоторое время ее не трогали, и поэтому скорость процессора ограничена скоростью диска

Вы можете проверить это с помощью Process Explorer и проверить, сколько памяти кэшируется.

еврей
источник
Ты думаешь? Свободной памяти достаточно. Почему Windows начинает обмениваться? В любом случае, спасибо.
Нильс
По крайней мере, на моем ноутбуке Windows меняет местами свернутые приложения, даже с достаточным объемом памяти
Еврей,
0

Я думаю, что эта разница в производительности связана с тем, как ОС управляет потоками. JVM скрывает все различия ОС. Есть много сайтов , где вы можете прочитать о нем, как это , например. Но это не значит, что разница исчезает.

Я полагаю, вы работаете на Java 8+ JVM. В связи с этим я предлагаю вам попробовать использовать функции потокового и функционального программирования. Функциональное программирование очень полезно, когда у вас много мелких независимых проблем, и вы хотите легко переключаться с последовательного на параллельное выполнение. Хорошей новостью является то, что вам не нужно определять политику, чтобы определить, сколько потоков вы должны управлять (например, с помощью ExecutorService). Просто для примера (взято отсюда ):

package com.mkyong.java8;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class ParallelExample4 {

    public static void main(String[] args) {

        long count = Stream.iterate(0, n -> n + 1)
                .limit(1_000_000)
                //.parallel()   with this 23s, without this 1m 10s
                .filter(ParallelExample4::isPrime)
                .peek(x -> System.out.format("%s\t", x))
                .count();

        System.out.println("\nTotal: " + count);

    }

    public static boolean isPrime(int number) {
        if (number <= 1) return false;
        return !IntStream.rangeClosed(2, number / 2).anyMatch(i -> number % i == 0);
    }

}

Результат:

Для обычных потоков это занимает 1 минуту 10 секунд. Для параллельных потоков это занимает 23 секунды. PS Протестировано с i7-7700, 16G RAM, Windows 10

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

xcesco
источник
Я использую потоки в других частях программного обеспечения, но в этом случае задачи создаются при обходе графа. Я не знаю, как обернуть это с помощью потоков.
Нильс
Можете ли вы пройтись по графику, построить список и затем использовать потоки?
xcesco
Параллельные потоки являются только синтаксическим сахаром для ForkJoinPool. Это я попробовал (см. Комментарий @KarolDowbecki выше).
Нильс
0

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

Когда вы говорите, что загрузка процессора составляет 25%, вы имеете в виду, что одновременно работают только несколько ядер? (Может случиться так, что все ядра работают время от времени, но не одновременно.) Вы бы проверили, сколько потоков (или процессов) действительно создано в системе? Всегда ли число больше количества ядер?

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

Сяо-Фенг Ли
источник
Я добавил скриншот диспетчера задач для выполнения, которое является представителем этой проблемы. Само приложение создает столько потоков, сколько имеется физических ядер на компьютере. Java вносит чуть более 50 потоков в эту цифру. Как уже говорилось, VisualVM говорит, что все потоки заняты (зеленый). Они просто не нагружают процессор до предела в Windows. Они делают на Linux.
Нильс
@ Nils Я подозреваю, что на самом деле не все темы заняты одновременно , но на самом деле только 9 - 10 из них. Они распределяются случайным образом по всем ядрам, поэтому в среднем вы используете коэффициент использования 9/44 = 20%. Можете ли вы использовать потоки Java напрямую, а не ExecutorService, чтобы увидеть разницу? Нетрудно создать 44 потока, каждый из которых получает Runnable / Callable из пула / очереди задач. (Несмотря на то, что VisualVM показывает, что все потоки Java заняты, реальность может заключаться в том, что 44 потока планируются быстро, так что все они получают возможность работать в период выборки VisualVM.)
Сяо-Фенг Ли
Это мысль и кое-что, что я действительно сделал в какой-то момент. В моей реализации я также позаботился о том, чтобы собственный доступ был локальным для каждого потока, но это не имело никакого значения.
Нильс