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
anstattExecutorService
? 25% CPU-Auslastung ist sehr gering, wenn Ihr Problem CPU-gebunden ist.ForkJoinPool
ist dies effizienter als die manuelle Planung.Antworten:
Unter Windows ist die Anzahl der Threads pro Prozess durch den Adressraum des Prozesses begrenzt (siehe auch Mark Russinovich - Pushing the Limits of Windows: Prozesse und Threads ). Denken Sie, dass dies Nebenwirkungen verursacht, wenn es sich den Grenzen nähert (Verlangsamung der Kontextwechsel, Fragmentierung ...). Für Windows würde ich versuchen, die Arbeitslast auf eine Reihe von Prozessen aufzuteilen. Für ein ähnliches Problem, das ich vor Jahren hatte, habe ich eine Java-Bibliothek implementiert, um dies bequemer zu tun (Java 8). Schauen Sie sich das an, wenn Sie möchten: Bibliothek, um Aufgaben in einem externen Prozess zu erzeugen .
quelle
Klingt so, als würde Windows nach längerer Unberührtheit Speicher in der Auslagerungsdatei zwischenspeichern, weshalb die CPU durch die Festplattengeschwindigkeit einen Engpass aufweist
Sie können dies mit dem Process Explorer überprüfen und überprüfen, wie viel Speicher zwischengespeichert ist
quelle
Ich denke, dieser Leistungsunterschied ist darauf zurückzuführen, wie das Betriebssystem die Threads verwaltet. JVM verbirgt alle Betriebssystemunterschiede. Es gibt viele Orte , an denen man darüber, wie lesen kann dies zum Beispiel. Dies bedeutet jedoch nicht, dass der Unterschied verschwindet.
Ich nehme an, Sie laufen auf Java 8+ JVM. Aus diesem Grund empfehle ich Ihnen, Stream- und funktionale Programmierfunktionen zu verwenden. Funktionale Programmierung ist sehr nützlich, wenn Sie viele kleine unabhängige Probleme haben und einfach von der sequentiellen zur parallelen Ausführung wechseln möchten. Die gute Nachricht ist, dass Sie keine Richtlinie definieren müssen, um zu bestimmen, wie viele Threads Sie verwalten müssen (wie beim ExecutorService). Nur zum Beispiel (von hier genommen ):
Ich schlage daher vor, dass Sie sich über Funktionsprogrammierung, Stream und Lambda-Funktion in Java informieren und versuchen, eine kleine Anzahl von Tests mit Ihrem Code zu implementieren (angepasst, um in diesem neuen Kontext zu funktionieren).
quelle
Würden Sie bitte die Systemstatistik veröffentlichen? Der Task-Manager ist gut genug, um einen Hinweis zu geben, wenn dies das einzige verfügbare Tool ist. Es kann leicht festgestellt werden, ob Ihre Aufgaben auf E / A warten - was nach dem, was Sie beschrieben haben, wie der Schuldige klingt. Dies kann auf ein bestimmtes Speicherverwaltungsproblem zurückzuführen sein, oder die Bibliothek schreibt möglicherweise temporäre Daten auf die Festplatte usw.
Wenn Sie 25% der CPU-Auslastung angeben, bedeutet dies, dass nur wenige Kerne gleichzeitig beschäftigt sind? (Es kann sein, dass alle Kerne von Zeit zu Zeit funktionieren, jedoch nicht gleichzeitig.) Würden Sie überprüfen, wie viele Threads (oder Prozesse) tatsächlich im System erstellt werden? Ist die Anzahl immer größer als die Anzahl der Kerne?
Wenn es genügend Threads gibt, warten viele von ihnen im Leerlauf auf etwas? Wenn dies der Fall ist, können Sie versuchen, einen Debugger zu unterbrechen (oder anzuhängen), um zu sehen, worauf sie warten.
quelle