Zbyt niskie użycie procesora wielowątkowej aplikacji Java w systemie Windows

18

Pracuję nad aplikacją Java do rozwiązywania problemów z optymalizacją numeryczną - a dokładniej - problemów programowania liniowego na dużą skalę. Pojedynczy problem można podzielić na mniejsze podproblemy, które można rozwiązać równolegle. Ponieważ jest więcej podproblemów niż rdzeni procesora, używam ExecutorService i definiuję każdy podproblem jako wywoływalny, który jest przesyłany do ExecutorService. Rozwiązanie podproblemu wymaga wywołania biblioteki natywnej - w tym przypadku liniowego solvera programistycznego.

Problem

Mogę uruchomić aplikację w systemach Unix i Windows z maksymalnie 44 rdzeniami fizycznymi i pamięcią do 256 g, ale czasy obliczeń w systemie Windows są o rząd wielkości wyższe niż w przypadku Linuksa w przypadku dużych problemów. Windows wymaga nie tylko znacznie więcej pamięci, ale wykorzystanie procesora z czasem spada z 25% na początku do 5% po kilku godzinach. Oto zrzut ekranu menedżera zadań w systemie Windows:

Wykorzystanie procesora przez Menedżera zadań

Spostrzeżenia

  • Czasy rozwiązania dla dużych instancji ogólnego problemu wahają się od godzin do dni i zużywają do 32 g pamięci (w systemie Unix). Czasy rozwiązania dla podproblemu mieszczą się w zakresie ms.
  • Nie spotykam tego problemu w przypadku drobnych problemów, których rozwiązanie zajmuje tylko kilka minut.
  • Linux używa obu gniazd od razu po wyjęciu z pudełka, podczas gdy Windows wymaga ode mnie jawnej aktywacji przeplatania pamięci w systemie BIOS, aby aplikacja korzystała z obu rdzeni. Niezależnie od tego, czy to zrobię, nie ma to wpływu na pogorszenie ogólnego wykorzystania procesora w miarę upływu czasu.
  • Kiedy patrzę na wątki w VisualVM, wszystkie wątki z puli są uruchomione, żaden nie czeka.
  • Według VisualVM 90% czasu procesora jest przeznaczane na natywne wywołanie funkcji (rozwiązanie małego programu liniowego)
  • Odśmiecanie nie stanowi problemu, ponieważ aplikacja nie tworzy i nie odwołuje wielu obiektów. Ponadto wydaje się, że większość pamięci jest przydzielana poza stertą. W przypadku największej instancji wystarcza 4 g sterty w systemie Linux i 8 g w systemie Windows.

Co próbowałem

  • wszelkiego rodzaju argumenty JVM, wysokie XMS, wysokie metaspace, flaga UseNUMA, inne GC.
  • różne maszyny JVM (Hotspot 8, 9, 10, 11).
  • różne natywne biblioteki różnych liniowych solverów programistycznych (CLP, Xpress, Cplex, Gurobi).

pytania

  • Co wpływa na różnicę wydajności między Linuksem a Windowsem w wielowątkowej aplikacji Java, która intensywnie wykorzystuje natywne połączenia?
  • Czy jest coś, co mogę zmienić w implementacji, co pomogłoby systemowi Windows, na przykład, czy powinienem unikać korzystania z usługi ExecutorService, która odbiera tysiące wywołań, i zamiast tego robić?
Nils
źródło
Próbowałeś ForkJoinPoolzamiast ExecutorService? 25% wykorzystania procesora jest naprawdę niskie, jeśli twój problem jest związany z procesorem.
Karol Dowbecki
1
Twój problem brzmi jak coś, co powinno zwiększyć procesor do 100%, a jednak masz 25%. W przypadku niektórych problemów ForkJoinPooljest bardziej wydajny niż planowanie ręczne.
Karol Dowbecki
2
Przeglądając wersje Hotspot, czy upewniłeś się, że korzystasz z wersji „serwer”, a nie „klient”? Jakie jest wykorzystanie procesora w systemie Linux? Imponujący jest także czas pracy systemu przez kilka dni! Jaki jest Twój sekret? : P
erickson
3
Może spróbuj użyć Xperf do wygenerowania FlameGraph . To może dać ci wgląd w to, co robi procesor (mam nadzieję, że zarówno tryb użytkownika, jak i jądra), ale nigdy nie zrobiłem tego w systemie Windows.
Karol Dowbecki
1
@Nils, oba biegi (unix / win) używają tego samego interfejsu do wywoływania biblioteki natywnej? Pytam, bo wygląda inaczej. Jak: win używa jna, linux jni.
SR

Odpowiedzi:

2

W systemie Windows liczba wątków na proces jest ograniczona przestrzenią adresową procesu (patrz także Mark Russinovich - Przesuwanie granic systemu Windows: procesy i wątki ). Pomyśl, że powoduje to skutki uboczne, gdy zbliża się do granic (spowolnienie przełączania kontekstu, fragmentacja ...). W systemie Windows starałbym się podzielić obciążenie pracą na zestaw procesów. W przypadku podobnego problemu, który miałem lata temu, zaimplementowałem bibliotekę Java, aby zrobić to wygodniej (Java 8), spójrz, jeśli chcesz: Biblioteka do odradzania zadań w procesie zewnętrznym .

geri
źródło
To wygląda bardzo interesująco! Nie jestem pewien, czy posunąć się tak daleko (jeszcze) z dwóch powodów: 1) wystąpi narzut związany z serializacją i wysyłaniem obiektów przez gniazda; 2) jeśli chcę serializować wszystko, to obejmuje wszystkie zależności, które są połączone w zadaniu - przepisanie kodu byłoby trochę pracy - mimo to dziękuję za przydatne linki.
Nils
W pełni podzielam twoje obawy i przeprojektowanie kodu byłoby pewnym wysiłkiem. Podczas przemierzania wykresu należy wprowadzić próg liczby wątków, gdy nadejdzie czas podzielenia pracy na nowy podproces. Aby rozwiązać problem 2), spójrz na plik mapowany w pamięci Java (java.nio.MappedByteBuffer), dzięki któremu możesz efektywnie współdzielić dane między procesami, na przykład dane wykresu. Godspeed :)
geri
0

Wygląda na to, że Windows buforuje trochę pamięci do pliku stronicowania, po tym jak był przez jakiś czas nietknięty, i dlatego szybkość dysku jest wąska

Możesz to sprawdzić za pomocą Eksploratora procesów i sprawdzić, ile pamięci jest buforowane

Żyd
źródło
Myślisz? Jest wystarczająca ilość wolnej pamięci. Dlaczego Windows miałby zacząć zamianę? W każdym razie dzięki.
Nils,
Przynajmniej na oknach mojego laptopa zamienia się czasami zminimalizowane aplikacje, nawet z wystarczającą pamięcią
Żyd
0

Myślę, że ta różnica wydajności wynika z tego, jak system operacyjny zarządza wątkami. JVM ukrywa wszystkie różnice w systemie operacyjnym. Istnieje wiele miejsc, gdzie można przeczytać o tym, jak ten , na przykład. Ale to nie znaczy, że różnica znika.

Podejrzewam, że korzystasz z JVM Java 8+. Z tego powodu proponuję, abyś spróbował użyć funkcji programowania strumieniowego i funkcjonalnego. Programowanie funkcjonalne jest bardzo przydatne, gdy masz wiele małych niezależnych problemów i chcesz łatwo przełączyć się z wykonywania sekwencyjnego na równoległe. Dobrą wiadomością jest to, że nie musisz definiować polityki, aby określić, ile wątków musisz zarządzać (jak w przypadku usługi ExecutorService). Na przykład (wzięte stąd ):

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);
    }

}

Wynik:

W przypadku normalnych strumieni zajmuje to 1 minutę i 10 sekund. W przypadku strumieni równoległych zajmuje to 23 sekundy. PS Testowane z i7-7700, 16G RAM, Windows 10

Proponuję więc przeczytać o programowaniu funkcji, streamie, funkcji lambda w Javie i spróbować zaimplementować niewielką liczbę testów za pomocą kodu (przystosowanych do pracy w tym nowym kontekście).

Xcesco
źródło
Używam strumieni w innych częściach oprogramowania, ale w tym przypadku zadania są tworzone podczas przeglądania wykresu. Nie wiedziałbym, jak to owinąć za pomocą strumieni.
Nils,
Czy potrafisz przemierzać wykres, budować listę, a następnie używać strumieni?
xcesco,
Strumienie równoległe to tylko cukier syntaktyczny dla ForkJoinPool. Że próbowałem (patrz komentarz @KololDowbecki powyżej).
Nils,
0

Czy mógłbyś opublikować statystyki systemu? Menedżer zadań jest wystarczająco dobry, aby dostarczyć wskazówek, jeśli jest to jedyne dostępne narzędzie. Może łatwo stwierdzić, czy twoje zadania czekają na IO - co brzmi jak winowajca na podstawie tego, co opisałeś. Może to wynikać z pewnych problemów z zarządzaniem pamięcią lub biblioteka może zapisać tymczasowe dane na dysku itp.

Kiedy mówisz o 25% wykorzystania procesora, czy masz na myśli, że tylko kilka rdzeni jest zajętych jednocześnie? (Może się zdarzyć, że wszystkie rdzenie działają od czasu do czasu, ale nie jednocześnie.) Czy sprawdziłbyś, ile wątków (lub procesów) jest naprawdę tworzonych w systemie? Czy liczba jest zawsze większa niż liczba rdzeni?

Jeśli jest wystarczająco dużo wątków, czy wiele z nich jest bezczynnych i czeka na coś? Jeśli to prawda, możesz spróbować przerwać (lub dołączyć debugger), aby zobaczyć, na co czekają.

Xiao-Feng Li
źródło
Dodałem zrzut ekranu menedżera zadań dla wykonania reprezentatywnego dla tego problemu. Sama aplikacja tworzy tyle wątków, ile jest fizycznych rdzeni na maszynie. Java wnosi do tej liczby nieco ponad 50 wątków. Jak już wspomniano, VisualVM mówi, że wszystkie wątki są zajęte (zielone). Po prostu nie przesuwają procesora do granic możliwości w systemie Windows. Robią w systemie Linux.
Nils,
@Nils Podejrzewam, że tak naprawdę nie masz wszystkich zajętych wątków w tym samym czasie, ale w rzeczywistości tylko 9-10 z nich. Są one planowane losowo we wszystkich rdzeniach, stąd średnie wykorzystanie wynosi 9/44 = 20%. Czy możesz użyć wątków Java bezpośrednio zamiast ExecutorService, aby zobaczyć różnicę? Utworzenie 44 wątków nie jest trudne, a każdy z nich pobiera Runnable / Callable z puli zadań / kolejki. (Chociaż VisualVM pokazuje, że wszystkie wątki Java są zajęte, rzeczywistość może być taka, że ​​44 wątki są planowane szybko, aby wszystkie miały szansę na uruchomienie w okresie próbkowania VisualVM.)
Xiao-Feng Li
To myśl i coś, co faktycznie zrobiłem w pewnym momencie. W mojej implementacji upewniłem się również, że natywny dostęp jest lokalny dla każdego wątku, ale nie miało to żadnego znaczenia.
Nils,