Ich arbeite an einer Java-Anwendung zur Lösung einer Klasse numerischer Optimierungsprobleme - genauer gesagt bei großen linearen Programmierproblemen. Ein einzelnes Problem kann in kleinere Teilprobleme aufgeteilt werden, die parallel gelöst werden können. Da es mehr Unterprobleme als CPU-Kerne gibt, verwende ich einen ExecutorService und definiere jedes Unterproblem als Callable, das an den ExecutorService gesendet wird. Um ein Teilproblem zu lösen, muss eine native Bibliothek aufgerufen werden - in diesem Fall ein linearer Programmierlöser.
Problem
Ich kann die Anwendung unter Unix und auf Windows-Systemen mit bis zu 44 physischen Kernen und bis zu 256 g Speicher ausführen, aber die Rechenzeiten unter Windows sind bei großen Problemen um eine Größenordnung höher als unter Linux. Windows benötigt nicht nur wesentlich mehr Speicher, sondern die CPU-Auslastung sinkt im Laufe der Zeit von 25% am Anfang auf 5% nach einigen Stunden. Hier ist ein Screenshot des Task-Managers in Windows:
Beobachtungen
- Die Lösungszeiten für große Instanzen des Gesamtproblems reichen von Stunden bis zu Tagen und verbrauchen bis zu 32 g Speicher (unter Unix). Die Lösungszeiten für ein Teilproblem liegen im ms-Bereich.
- Ich stoße nicht auf dieses Problem bei kleinen Problemen, deren Lösung nur wenige Minuten dauert.
- Linux verwendet beide Sockets sofort, während Windows verlangt, dass ich die Speicherverschachtelung im BIOS explizit aktiviere, damit die Anwendung beide Kerne verwendet. Ob ich dies nicht tue, hat jedoch keinen Einfluss auf die Verschlechterung der gesamten CPU-Auslastung im Laufe der Zeit.
- Wenn ich mir die Threads in VisualVM ansehe, werden alle Pool-Threads ausgeführt, keiner wartet oder sonst.
- Laut VisualVM werden 90% der CPU-Zeit für einen nativen Funktionsaufruf (Lösen eines kleinen linearen Programms) aufgewendet.
- Die Speicherbereinigung ist kein Problem, da die Anwendung nicht viele Objekte erstellt und deren Referenzierung aufhebt. Außerdem scheint der größte Teil des Speichers außerhalb des Heapspeichers zugewiesen zu sein. 4 g Heap reichen unter Linux und 8 g unter Windows für die größte Instanz aus.
Was ich versucht habe
- Alle Arten von JVM-Argumenten, High XMS, High Metaspace, UseNUMA-Flag und andere GCs.
- verschiedene JVMs (Hotspot 8, 9, 10, 11).
- verschiedene native Bibliotheken verschiedener linearer Programmierlöser (CLP, Xpress, Cplex, Gurobi).
Fragen
- Was treibt den Leistungsunterschied zwischen Linux und Windows einer großen Multithread-Java-Anwendung an, die native Aufrufe stark nutzt?
- Gibt es irgendetwas, das ich an der Implementierung ändern kann, das Windows helfen würde, sollte ich beispielsweise vermeiden, einen ExecutorService zu verwenden, der Tausende von Callables empfängt, und stattdessen was tun?
ForkJoinPool
ist dies effizienter als die manuelle Planung.
ForkJoinPool
anstattExecutorService
? 25% CPU-Auslastung ist sehr gering, wenn Ihr Problem CPU-gebunden ist.