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