Zu geringe CPU-Auslastung von Multithread-Java-Anwendungen unter Windows

18

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:

Task-Manager-CPU-Auslastung

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?
Nils
quelle
Hast du es versucht ForkJoinPoolanstatt ExecutorService? 25% CPU-Auslastung ist sehr gering, wenn Ihr Problem CPU-gebunden ist.
Karol Dowbecki
1
Ihr Problem klingt nach etwas, das die CPU auf 100% bringen sollte, und dennoch sind Sie auf 25%. Bei einigen Problemen ForkJoinPoolist dies effizienter als die manuelle Planung.
Karol Dowbecki
2
Haben Sie beim Durchlaufen von Hotspot-Versionen sichergestellt, dass Sie die Version "Server" und nicht "Client" verwenden? Wie hoch ist Ihre CPU-Auslastung unter Linux? Auch die Windows-Betriebszeit von mehreren Tagen ist beeindruckend! Was ist dein Geheimnis? : P
erickson
3
Vielleicht versuchen Sie es mit Xperf eine erzeugen FlameGraph . Dies könnte Ihnen einen Einblick geben, was die CPU tut (hoffentlich sowohl im Benutzer- als auch im Kernel-Modus), aber ich habe es unter Windows nie gemacht.
Karol Dowbecki
1
@Nils, beide Läufe (Unix / Win) verwenden dieselbe Schnittstelle, um die native Bibliothek aufzurufen? Ich frage, weil es anders aussieht. Wie: win verwendet jna, linux jni.
SR

Antworten:

2

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 .

geri
quelle
Das sieht sehr interessant aus! Ich zögere (noch) aus zwei Gründen, so weit zu gehen: 1) Das Serialisieren und Senden von Objekten über Sockets wird einen Leistungsaufwand verursachen. 2) Wenn ich alles serialisieren möchte, einschließlich aller Abhängigkeiten, die in einer Aufgabe verknüpft sind, wäre es ein wenig Arbeit, den Code neu zu schreiben. Trotzdem danke ich Ihnen für die nützlichen Links.
Nils
Ich teile Ihre Bedenken voll und ganz und die Neugestaltung des Codes wäre einige Anstrengungen. Beim Durchlaufen des Diagramms müssten Sie einen Schwellenwert für die Anzahl der Threads einführen, wenn die Arbeit in einen neuen Unterprozess aufgeteilt werden soll. Um 2 zu adressieren, werfen Sie einen Blick auf die Java-Speicherzuordnungsdatei (java.nio.MappedByteBuffer), mit der Sie Daten effektiv zwischen Prozessen austauschen können, z. B. Ihre Diagrammdaten. Godspeed :)
Geri
0

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

Jude
quelle
Du denkst? Es ist genügend freier Speicher vorhanden. Warum sollte Windows anfangen zu tauschen? Trotzdem danke.
Nils
Zumindest auf meinem Laptop tauscht Windows manchmal minimierte Anwendungen aus, selbst mit genügend Speicher
Jude
0

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 ):

package com.mkyong.java8;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class ParallelExample4 {

    public static void main(String[] args) {

        long count = Stream.iterate(0, n -> n + 1)
                .limit(1_000_000)
                //.parallel()   with this 23s, without this 1m 10s
                .filter(ParallelExample4::isPrime)
                .peek(x -> System.out.format("%s\t", x))
                .count();

        System.out.println("\nTotal: " + count);

    }

    public static boolean isPrime(int number) {
        if (number <= 1) return false;
        return !IntStream.rangeClosed(2, number / 2).anyMatch(i -> number % i == 0);
    }

}

Ergebnis:

Bei normalen Streams dauert es 1 Minute und 10 Sekunden. Bei parallelen Streams dauert es 23 Sekunden. PS Getestet mit i7-7700, 16G RAM, Windows 10

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).

xcesco
quelle
Ich verwende Streams in anderen Teilen der Software, aber in diesem Fall werden Aufgaben beim Durchlaufen eines Diagramms erstellt. Ich würde nicht wissen, wie man dies mit Streams umschließt.
Nils
Können Sie das Diagramm durchlaufen, eine Liste erstellen und dann Streams verwenden?
Xcesco
Parallele Streams sind nur syntaktischer Zucker für einen ForkJoinPool. Das habe ich versucht (siehe @KarolDowbecki Kommentar oben).
Nils
0

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.

Xiao-Feng Li
quelle
Ich habe einen Screenshot des Task-Managers für eine Ausführung hinzugefügt, die für dieses Problem repräsentativ ist. Die Anwendung selbst erstellt so viele Threads, wie physische Kerne auf dem Computer vorhanden sind. Java trägt etwas mehr als 50 Threads zu dieser Zahl bei. Wie bereits erwähnt, sagt VisualVM, dass alle Threads beschäftigt sind (grün). Sie bringen die CPU unter Windows einfach nicht an ihre Grenzen. Sie tun unter Linux.
Nils
@Nils Ich vermute, Sie haben nicht wirklich alle Threads gleichzeitig beschäftigt, sondern nur 9 - 10 davon. Sie werden zufällig über alle Kerne hinweg geplant, daher haben Sie im Durchschnitt eine Auslastung von 9/44 = 20%. Können Sie Java-Threads direkt anstelle von ExecutorService verwenden, um den Unterschied zu erkennen? Es ist nicht schwierig, 44 Threads zu erstellen, die jeweils Runnable / Callable aus einem Aufgabenpool / einer Warteschlange abrufen. (Obwohl VisualVM zeigt, dass alle Java-Threads ausgelastet sind, kann die Realität sein, dass die 44 Threads schnell geplant werden, damit alle in der Abtastperiode von VisualVM ausgeführt werden können.)
Xiao-Feng Li
Das ist ein Gedanke und etwas, das ich tatsächlich irgendwann getan habe. In meiner Implementierung habe ich auch sichergestellt, dass der native Zugriff für jeden Thread lokal ist, dies hat jedoch überhaupt keinen Unterschied gemacht.
Nils