Почему Java не использует все мои ядра процессора эффективно
Я запускаю Ubuntu на машине с четырехъядерным процессором. Я написал некоторый тестовый Java-код, который порождает определенное количество процессов, которые просто увеличивают изменчивую переменную для заданного количества итераций при запуске.
Я ожидал бы, что время выполнения не увеличится значительно, а количество потоков меньше или равно числу ядер, т.е. 4. Фактически, это время, когда я получаю "реальное время" из UNIX time
:
1 thread: 1.005s
2 потока: 1.018s
3 темы: 1.528s
4 потока: 1.982s
5 нитей: 2.479s
6 нитей: 2.934s
7 потоков: 3.356s
8 нитей: 3.793s
Это показывает, что добавление одного дополнительного потока не увеличивает время, как ожидалось, но тогда время увеличивается с 3 и 4 потоками.
Сначала я думал, что это может быть связано с тем, что ОС препятствует использованию JVM всех ядер, но я побежал top
, и он ясно показал, что с 3 потоками 3 ядра работали на ~ 100%, а с 4 потока, 4 ядра были превышены.
Мой вопрос: почему код работает на 3/4 процессорах не примерно такой же, как при работе на 1/2? Поскольку он работает параллельно на всех ядрах.
Вот мой основной метод для справки:
class Example implements Runnable {
// using this so the compiler does not optimise the computation away
volatile int temp;
void delay(int arg) {
for (int i = 0; i < arg; i++) {
for (int j = 0; j < 1000000; j++) {
this.temp += i + j;
}
}
}
int arg;
int result;
Example(int arg) {
this.arg = arg;
}
public void run() {
delay(arg);
result = 42;
}
public static void main(String args[]) {
// Get the number of threads (the command line arg)
int numThreads = 1;
if (args.length > 0) {
try {
numThreads = Integer.parseInt(args[0]);
} catch (NumberFormatException nfe) {
System.out.println("First arg must be the number of threads!");
}
}
// Start up the threads
Thread[] threadList = new Thread[numThreads];
Example[] exampleList = new Example[numThreads];
for (int i = 0; i < numThreads; i++) {
exampleList[i] = new Example(1000);
threadList[i] = new Thread(exampleList[i]);
threadList[i].start();
}
// wait for the threads to finish
for (int i = 0; i < numThreads; i++) {
try {
threadList[i].join();
System.out.println("Joined with thread, ret=" + exampleList[i].result);
} catch (InterruptedException ie) {
System.out.println("Caught " + ie);
}
}
}
}
Ответы
Ответ 1
Core i5 в Lenovo X1 Carbon не является четырехъядерным процессором. Это двухъядерный процессор с гиперпотоком. Когда вы выполняете только тривиальные операции, которые не приводят к частым длинным конвейерным стойкам, тогда планировщик гиперпотоков не будет иметь большой возможности перетащить другие операции в заторможенный конвейер, и вы не увидите производительность, эквивалентную четырем реальным ядрам.
Ответ 2
Использование нескольких ЦП помогает до такой степени, что вы насыщаете некоторый базовый ресурс.
В вашем случае основным ресурсом является не количество процессоров, а количество кэшей L1, которые у вас есть. В вашем случае, похоже, у вас есть два ядра, каждый из которых кэша данных L1, и поскольку вы нажимаете на него с изменчивой записью, то здесь вам нужны лимиты L1.
Попробуйте получить доступ к кэшу L1 меньше
public class Example implements Runnable {
// using this so the compiler does not optimise the computation away
volatile int temp;
void delay(int arg) {
for (int i = 0; i < arg; i++) {
int temp = 0;
for (int j = 0; j < 1000000; j++) {
temp += i + j;
}
this.temp += temp;
}
}
int arg;
int result;
Example(int arg) {
this.arg = arg;
}
public void run() {
delay(arg);
result = 42;
}
public static void main(String... ignored) {
int MAX_THREADS = Integer.getInteger("max.threads", 8);
long[] times = new long[MAX_THREADS + 1];
for (int numThreads = MAX_THREADS; numThreads >= 1; numThreads--) {
long start = System.nanoTime();
// Start up the threads
Thread[] threadList = new Thread[numThreads];
Example[] exampleList = new Example[numThreads];
for (int i = 0; i < numThreads; i++) {
exampleList[i] = new Example(1000);
threadList[i] = new Thread(exampleList[i]);
threadList[i].start();
}
// wait for the threads to finish
for (int i = 0; i < numThreads; i++) {
try {
threadList[i].join();
System.out.println("Joined with thread, ret=" + exampleList[i].result);
} catch (InterruptedException ie) {
System.out.println("Caught " + ie);
}
}
long time = System.nanoTime() - start;
times[numThreads] = time;
System.out.printf("%d: %.1f ms%n", numThreads, time / 1e6);
}
for (int i = 2; i <= MAX_THREADS; i++)
System.out.printf("%d: %.3f time %n", i, (double) times[i] / times[1]);
}
}
На моем двухъядерном, гиперпоточном ноутбуке он создается в форме threads: factor
2: 1.093 time
3: 1.180 time
4: 1.244 time
5: 1.759 time
6: 1.915 time
7: 2.154 time
8: 2.412 time
по сравнению с первоначальным тестом
2: 1.092 time
3: 2.198 time
4: 3.349 time
5: 3.079 time
6: 3.556 time
7: 4.183 time
8: 4.902 time
Общим ресурсом для более эффективного использования является кеш L3. Это распределяется между центральными процессорами, и хотя он допускает степень concurrency, он не масштабируется значительно выше, чем у процессоров. Я предлагаю вам проверить, что делает ваш пример кода, и убедиться, что они могут работать независимо и не использовать общие ресурсы. например Большинство чипов имеют ограниченное количество FPU.
Ответ 3
Есть несколько вещей, которые могут ограничить, насколько эффективно вы можете многопоточно использовать приложение.
-
Насыщенность ресурса, такого как пропускная способность памяти/шины/etc.
-
Проблемы с блокировкой/конфликтом (например, если потокам постоянно приходится ждать друг друга, чтобы закончить).
-
Другие процессы, запущенные в системе.
В вашем случае вы используете переменное целое число, к которому обращаются все потоки, что означает, что потоки постоянно должны отправлять новое значение этого целого между собой. Это вызовет некоторый уровень конкуренции и использования памяти/полосы пропускания.
Попробуйте переключить каждый поток, чтобы работать на собственной части данных без переменной volatile. Это должно уменьшить все формы разногласий.
Ответ 4
Если вы используете это на Core i5 (столько, сколько Google сообщает мне о Lenovo X1 Carbon), то у вас есть двухъядерная машина с двумя гипер-ядрами. Отчеты i5 для ОС - и, следовательно, для Java - как четырехъядерные, поэтому гиперядро используется как реальные ядра, но все это делается для ускорения переключения потоков.
Вот почему вы получаете ожидаемую минимальную разницу во времени выполнения с двумя потоками (1 на реальное ядро) и почему время не растет линейно с дополнительными потоками, потому что 2 гипер-ядра получают небольшую нагрузку от реального ядра.
Ответ 5
Уже есть два хороших ответа, оба прекрасно объясняют, что происходит.
Посмотрите на процессор, большая часть "четырехъядерного ядра" от Intel на самом деле является двухъядерным процессором, который имитирует четырехъядерный ядро ОС (да, они говорят вам, что у вас есть 4 ядра, но у вас всего 2 факт...). Это лучшее объяснение вашей проблемы, потому что время увеличивается как двухъядерный процессор.
Если у вас есть реальный 4 ядра, другой ответ заключается в том, что у вас есть код concurrency.