¿Por qué Java no utiliza todos mis núcleos de CPU de forma efectiva
multithreading concurrency (5)
El Core i5 en un Lenovo X1 Carbon no es un procesador de cuatro núcleos. Es un procesador de dos núcleos con hyperthreading. Cuando realiza solo operaciones triviales que no dan lugar a paros frecuentes y prolongados, el programador de hipervínculos no tendrá muchas oportunidades de tejer otras operaciones en el conducto estancado y no verá un rendimiento equivalente a cuatro núcleos reales.
Esta pregunta ya tiene una respuesta aquí:
Estoy ejecutando Ubuntu en una máquina con una CPU de cuatro núcleos. He escrito un código Java de prueba que genera un número dado de procesos que simplemente incrementan una variable volátil para un número dado de iteraciones cuando se ejecutan.
Espero que el tiempo de ejecución no aumente significativamente mientras que el número de subprocesos sea menor o igual que el número de núcleos, es decir, 4. De hecho, estas son las veces que obtengo el uso del "tiempo real" del comando de time
UNIX:
1 hilo: 1.005s
2 hilos: 1.018s
3 hilos: 1.528s
4 hilos: 1.982s
5 hilos: 2.479s
6 hilos: 2.934s
7 hilos: 3.356s
8 hilos: 3.793s
Esto muestra que agregar un subproceso adicional no aumenta el tiempo como se esperaba, pero luego el tiempo aumenta con 3 y 4 subprocesos.
Al principio pensé que esto podría ser porque el sistema operativo impedía que la JVM usara todos los núcleos, pero funcioné top
, y se mostró claramente que con 3 subprocesos, 3 núcleos funcionaban al 100%, y con 4 subprocesos, 4 núcleos fueron llevados al máximo.
Mi pregunta es: ¿por qué el código que se ejecuta en las CPU 3/4 no es aproximadamente la misma velocidad que cuando se ejecuta en 1/2? Porque se está ejecutando en paralelo en todos los núcleos.
Aquí está mi principal método de referencia:
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);
}
}
}
}
El uso de varias CPU ayuda hasta el punto de saturar algunos recursos subyacentes.
En su caso, el recurso subyacente no es la cantidad de CPU sino la cantidad de cachés L1 que tiene. En su caso, parece que tiene dos núcleos, con un caché de datos L1 cada uno y, dado que lo está golpeando con una escritura volátil, los cachés L1 son su factor limitante.
Intenta acceder al caché L1 menos con
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]);
}
}
En mi portátil de doble núcleo, hyperthreaded, produce en los threads: factor
formularios 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
en comparación con la prueba original de
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
Un recurso común para sobreutilizar es el caché L3. Esto se comparte entre las CPU y si bien permite un cierto grado de concurrencia, no se escala mucho por encima de las CPU. Le sugiero que compruebe lo que está haciendo su código de Ejemplo y que se asegure de que puedan ejecutarse de forma independiente y no utilizar ningún recurso compartido. Por ejemplo, la mayoría de los chips tienen un número limitado de FPU.
Hay varias cosas que pueden limitar la eficacia con la que puede multiprocilar una aplicación.
Saturación de un recurso como el ancho de banda de memoria / bus / etc.
Problemas de bloqueo / contención (por ejemplo, si los subprocesos tienen que esperar constantemente para que terminen).
Otros procesos que se ejecutan en el sistema.
En su caso, está utilizando un entero volátil al que acceden todos los subprocesos, lo que significa que los subprocesos están constantemente teniendo que enviar el nuevo valor de ese entero entre ellos. Esto causará cierto nivel de contención y uso de memoria / ancho de banda.
Intente cambiar cada subproceso para que trabaje en su propia parte de datos sin variable volátil. Eso debería reducir todas las formas de contención.
Si está ejecutando esto en el Core i5 (tanto como Google me cuenta sobre el Lenovo X1 Carbon), entonces tiene una máquina de doble núcleo con 2 hipercoros. El i5 informa al sistema operativo, y por lo tanto a Java, como un quad-core, por lo que los hipercoros se utilizan como núcleos reales, pero todo lo que hacen es acelerar el cambio de contexto de subprocesos.
Es por eso que obtiene la diferencia mínima esperada en el tiempo de ejecución con 2 subprocesos (1 por núcleo real), y por qué el tiempo no aumenta linealmente con subprocesos adicionales, porque los 2 hipercoros toman una pequeña carga de los núcleos reales.
Ya hay dos buenas respuestas para usted, ambas están bien para explicar lo que está sucediendo.
Mire a su procesador, la mayor parte del "quad core" de Intel es en realidad un doble núcleo, que simula un sistema operativo de cuatro núcleos (sí, le dicen que tiene 4 núcleos, pero de hecho, solo tiene 2). .). Esta es la mejor explicación para su problema, porque el tiempo se incrementa como un procesador de doble núcleo.
Si tiene un núcleo real 4, la otra respuesta es que su código tiene alguna concurrencia.