Uso de CPU demasiado bajo de la aplicación Java multiproceso en Windows

18

Estoy trabajando en una aplicación Java para resolver una clase de problemas de optimización numérica: problemas de programación lineal a gran escala para ser más precisos. Un solo problema puede dividirse en subproblemas más pequeños que pueden resolverse en paralelo. Como hay más subproblemas que núcleos de CPU, uso un ExecutorService y defino cada subproblema como un invocable que se envía al ExecutorService. Resolver un subproblema requiere llamar a una biblioteca nativa, un solucionador de programación lineal en este caso.

Problema

Puedo ejecutar la aplicación en Unix y en sistemas Windows con hasta 44 núcleos físicos y hasta 256 g de memoria, pero los tiempos de cálculo en Windows son un orden de magnitud mayor que en Linux para grandes problemas. Windows no solo requiere sustancialmente más memoria, sino que la utilización de la CPU con el tiempo cae del 25% al ​​principio al 5% después de unas pocas horas. Aquí hay una captura de pantalla del administrador de tareas en Windows:

Uso de CPU del Administrador de tareas

Observaciones

  • Los tiempos de solución para grandes instancias del problema general varían de horas a días y consumen hasta 32 g de memoria (en Unix). Los tiempos de solución para un subproblema están en el rango de ms.
  • No encuentro este problema en pequeños problemas que tardan solo unos minutos en resolverse.
  • Linux usa ambos sockets listos para usar, mientras que Windows requiere que active explícitamente la intercalación de memoria en el BIOS para que la aplicación utilice ambos núcleos. Si no lo hago, esto no tiene ningún efecto sobre el deterioro de la utilización general de la CPU con el tiempo.
  • Cuando miro los subprocesos en VisualVM, todos los subprocesos del grupo se están ejecutando, ninguno está en espera o de lo contrario.
  • Según VisualVM, el 90% del tiempo de CPU se gasta en una llamada de función nativa (resolver un pequeño programa lineal)
  • La recolección de basura no es un problema, ya que la aplicación no crea y elimina la referencia de muchos objetos. Además, la mayoría de la memoria parece estar asignada fuera del montón. 4 g de almacenamiento dinámico son suficientes en Linux y 8 g en Windows para la instancia más grande.

Lo que he intentado

  • todo tipo de argumentos JVM, alto XMS, alto metaespacio, bandera UseNUMA, otros GC.
  • diferentes JVM (Hotspot 8, 9, 10, 11).
  • diferentes bibliotecas nativas de diferentes solucionadores de programación lineal (CLP, Xpress, Cplex, Gurobi).

Preguntas

  • ¿Qué impulsa la diferencia de rendimiento entre Linux y Windows de una gran aplicación Java multiproceso que hace un uso intensivo de las llamadas nativas?
  • ¿Hay algo que pueda cambiar en la implementación que ayude a Windows, por ejemplo, debería evitar usar un ExecutorService que recibe miles de Callables y hacer qué?
Nils
fuente
¿Has probado en ForkJoinPoollugar de ExecutorService? El 25% de utilización de la CPU es realmente bajo si su problema está vinculado a la CPU.
Karol Dowbecki
1
Su problema suena como algo que debería llevar la CPU al 100% y, sin embargo, está en el 25%. Para algunos problemas ForkJoinPooles más eficiente que la programación manual.
Karol Dowbecki
2
Al recorrer las versiones de Hotspot, ¿se ha asegurado de que está utilizando la versión "servidor" y no "cliente"? ¿Cuál es la utilización de tu CPU en Linux? Además, el tiempo de actividad de Windows de varios días es impresionante. ¿Cual es tu secreto? : P
erickson
3
Tal vez intente usar Xperf para generar un FlameGraph . Esto podría darle una idea de lo que está haciendo la CPU (con suerte tanto en modo de usuario como de kernel), pero nunca lo hice en Windows.
Karol Dowbecki
1
@Nils, ¿ambas ejecuciones (unix / win) usan la misma interfaz para llamar a la biblioteca nativa? Pregunto, porque parece diferente. Como: win usa jna, linux jni.
SR

Respuestas:

2

Para Windows, el número de subprocesos por proceso está limitado por el espacio de direcciones del proceso (ver también Mark Russinovich - Impulsando los límites de Windows: procesos y subprocesos ). Piense que esto causa efectos secundarios cuando se acerca a los límites (desaceleración de los cambios de contexto, fragmentación ...). Para Windows, trataría de dividir la carga de trabajo en un conjunto de procesos. Para un problema similar que tuve hace años, implementé una biblioteca Java para hacer esto más convenientemente (Java 8), eche un vistazo si lo desea: Biblioteca para generar tareas en un proceso externo .

geri
fuente
Esto se ve muy interesante! Dudo un poco en llegar tan lejos (todavía) por dos razones: 1) habrá una sobrecarga de rendimiento de serialización y envío de objetos a través de sockets; 2) si quiero serializar todo, esto incluye todas las dependencias que están vinculadas en una tarea, sería un poco difícil reescribir el código; no obstante, gracias por los enlaces útiles.
Nils
Comparto completamente sus preocupaciones y rediseñar el código sería un esfuerzo. Al atravesar el gráfico, deberá introducir un umbral para el número de subprocesos cuando llegue el momento de dividir el trabajo en un nuevo subproceso. Para abordar 2), eche un vistazo al archivo mapeado en memoria Java (java.nio.MappedByteBuffer), con el que podría compartir datos de manera efectiva entre procesos, por ejemplo, sus datos gráficos. Godspeed :)
geri
0

Parece que Windows está almacenando en caché algo de memoria en el archivo de paginación, después de que no se haya tocado durante algún tiempo, y es por eso que la velocidad del disco bloquea la CPU

Puede verificarlo con Process Explorer y verificar cuánta memoria se almacena en caché

judío
fuente
¿Crees? Hay suficiente memoria libre. ¿Por qué Windows comenzaría a intercambiar? De todos modos, gracias.
Nils
Al menos en mi computadora portátil, Windows está intercambiando aplicaciones a veces minimizadas, incluso con suficiente memoria
Judio
0

Creo que esta diferencia de rendimiento se debe a cómo el sistema operativo gestiona los hilos. JVM oculta toda la diferencia del sistema operativo. Hay muchos sitios donde puedes leer sobre esto, como este , por ejemplo. Pero eso no significa que la diferencia desaparezca.

Supongo que está ejecutando Java 8+ JVM. Debido a este hecho, le sugiero que intente utilizar las características de transmisión y de programación funcional. La programación funcional es muy útil cuando tiene muchos problemas pequeños e independientes y desea cambiar fácilmente de ejecución secuencial a ejecución paralela. La buena noticia es que no tiene que definir una política para determinar cuántos subprocesos debe administrar (como con el ExecutorService). Solo por ejemplo (tomado de aquí ):

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);
    }

}

Resultado:

Para transmisiones normales, toma 1 minuto y 10 segundos. Para flujos paralelos, toma 23 segundos. PS Probado con i7-7700, 16G RAM, Windows 10

Por lo tanto, le sugiero que lea sobre programación de funciones, transmisión, función lambda en Java e intente implementar una pequeña cantidad de pruebas con su código (adaptado para funcionar en este nuevo contexto).

xcesco
fuente
Utilizo flujos en otras partes del software, pero en este caso las tareas se crean al atravesar un gráfico. No sabría cómo envolver esto usando secuencias.
Nils
¿Puedes atravesar el gráfico, construir una lista y luego usar flujos?
xcesco
Las corrientes paralelas son solo azúcar sintáctico para un ForkJoinPool. Eso lo he intentado (ver el comentario de @KarolDowbecki arriba).
Nils
0

¿Podría publicar las estadísticas del sistema? El administrador de tareas es lo suficientemente bueno como para proporcionar alguna pista si esa es la única herramienta disponible. Puede determinar fácilmente si sus tareas están esperando IO, lo que parece ser el culpable según lo que describió. Puede deberse a cierto problema de administración de memoria, o la biblioteca puede escribir algunos datos temporales en el disco, etc.

Cuando dice 25% de la utilización de la CPU, ¿quiere decir que solo unos pocos núcleos están ocupados trabajando al mismo tiempo? (Puede ser que todos los núcleos funcionen de vez en cuando, pero no simultáneamente). ¿Verificaría cuántos hilos (o procesos) realmente se crean en el sistema? ¿El número siempre es mayor que el número de núcleos?

Si hay suficientes hilos, ¿muchos de ellos están inactivos esperando algo? Si es cierto, puede intentar interrumpir (o adjuntar un depurador) para ver qué están esperando.

Xiao-Feng Li
fuente
He agregado una captura de pantalla del administrador de tareas para una ejecución que es representativa de este problema. La aplicación en sí misma crea tantos subprocesos como núcleos físicos en la máquina. Java aporta un poco más de 50 hilos a esa cifra. Como ya se dijo, VisualVM dice que todos los hilos están ocupados (verde). Simplemente no llevan la CPU al límite en Windows. Lo hacen en Linux.
Nils
@Nils Sospecho que no tienes todos los hilos ocupados al mismo tiempo, pero en realidad solo 9-10 de ellos. Se programan aleatoriamente en todos los núcleos, por lo tanto, tiene un promedio de 9/44 = 20% de utilización. ¿Puedes usar hilos Java directamente en lugar de ExecutorService para ver la diferencia? No es difícil crear 44 subprocesos, y cada uno toma el Runnable / Callable de un grupo de tareas / cola. (Aunque VisualVM muestra que todos los subprocesos de Java están ocupados, la realidad puede ser que los 44 subprocesos se programen rápidamente para que todos tengan la oportunidad de ejecutarse en el período de muestreo de VisualVM.)
Xiao-Feng Li
Eso es un pensamiento y algo que realmente hice en algún momento. En mi implementación, también me aseguré de que el acceso nativo sea local para cada hilo, pero esto no hizo ninguna diferencia.
Nils