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:
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é?
ForkJoinPool
lugar deExecutorService
? El 25% de utilización de la CPU es realmente bajo si su problema está vinculado a la CPU.ForkJoinPool
es más eficiente que la programación manual.Respuestas:
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 .
fuente
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é
fuente
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í ):
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).
fuente
¿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.
fuente