Sto lavorando ad un'applicazione Java per risolvere una classe di problemi di ottimizzazione numerica - problemi di programmazione lineare su larga scala per essere più precisi. Un singolo problema può essere suddiviso in sottoproblemi più piccoli che possono essere risolti in parallelo. Poiché ci sono più sottoproblemi rispetto ai core della CPU, uso un ExecutorService e definisco ogni sottoproblema come un callable che viene inviato a ExecutorService. Per risolvere un sottoproblema è necessario chiamare una libreria nativa, in questo caso un risolutore di programmazione lineare.
Problema
Posso eseguire l'applicazione su sistemi Unix e Windows con un massimo di 44 core fisici e fino a 256 g di memoria, ma i tempi di calcolo su Windows sono di un ordine di grandezza superiore rispetto a Linux per problemi di grandi dimensioni. Windows non solo richiede molta più memoria, ma l'utilizzo della CPU nel tempo scende dal 25% all'inizio al 5% dopo poche ore. Ecco uno screenshot del task manager in Windows:
osservazioni
- I tempi di soluzione per grandi istanze dell'intero problema variano da ore a giorni e consumano fino a 32 g di memoria (su Unix). I tempi di soluzione per un sottoproblema sono compresi nell'intervallo ms.
- Non ho riscontrato questo problema su piccoli problemi che richiedono solo pochi minuti per risolverli.
- Linux usa entrambi i socket immediatamente, mentre Windows mi richiede di attivare esplicitamente l'interleaving della memoria nel BIOS in modo che l'applicazione utilizzi entrambi i core. Se non lo faccio, non ha alcun effetto sul deterioramento dell'utilizzo complessivo della CPU nel tempo.
- Quando guardo i thread in VisualVM tutti i thread del pool sono in esecuzione, nessuno è in attesa oppure.
- Secondo VisualVM, il 90% del tempo CPU viene impiegato per una chiamata di funzione nativa (risoluzione di un piccolo programma lineare)
- Garbage Collection non è un problema poiché l'applicazione non crea e de-referral molti oggetti. Inoltre, la maggior parte della memoria sembra essere allocata off-heap. 4 g di heap sono sufficienti su Linux e 8 g su Windows per l'istanza più grande.
Quello che ho provato
- tutti i tipi di argomenti JVM, XMS elevato, metaspace elevato, flag UseNUMA, altri GC.
- diverse JVM (Hotspot 8, 9, 10, 11).
- diverse librerie native di diversi solutori di programmazione lineare (CLP, Xpress, Cplex, Gurobi).
Domande
- Cosa determina la differenza di prestazioni tra Linux e Windows di una grande applicazione Java multi-thread che fa un uso pesante delle chiamate native?
- C'è qualcosa che posso cambiare nell'implementazione che potrebbe aiutare Windows, per esempio, dovrei evitare di usare un ExecutorService che riceve migliaia di callable e fare cosa invece?
ForkJoinPool
è più efficiente della pianificazione manuale.
ForkJoinPool
inveceExecutorService
? L'utilizzo della CPU del 25% è davvero basso se il tuo problema è legato alla CPU.