Utilisation trop faible du processeur de l'application Java multithread sous Windows

18

Je travaille sur une application Java pour résoudre une classe de problèmes d'optimisation numérique - des problèmes de programmation linéaire à grande échelle pour être plus précis. Un seul problème peut être divisé en sous-problèmes plus petits qui peuvent être résolus en parallèle. Puisqu'il y a plus de sous-problèmes que de cœurs CPU, j'utilise un ExecutorService et définit chaque sous-problème comme un Callable qui est soumis à l'ExecutorService. La résolution d'un sous-problème nécessite d'appeler une bibliothèque native - un solveur de programmation linéaire dans ce cas.

Problème

Je peux exécuter l'application sur Unix et sur les systèmes Windows avec jusqu'à 44 cœurs physiques et jusqu'à 256 g de mémoire, mais les temps de calcul sur Windows sont un ordre de grandeur plus élevés que sur Linux pour les gros problèmes. Windows nécessite non seulement beaucoup plus de mémoire, mais l'utilisation du processeur au fil du temps passe de 25% au début à 5% après quelques heures. Voici une capture d'écran du gestionnaire de tâches sous Windows:

Utilisation du processeur Task Manager

Observations

  • Les temps de solution pour les grandes instances du problème global vont de quelques heures à plusieurs jours et consomment jusqu'à 32 g de mémoire (sous Unix). Les temps de résolution d'un sous-problème sont de l'ordre de ms.
  • Je ne rencontre pas ce problème sur de petits problèmes qui ne prennent que quelques minutes à résoudre.
  • Linux utilise les deux sockets prêts à l'emploi, tandis que Windows m'oblige à activer explicitement l'entrelacement de la mémoire dans le BIOS pour que l'application utilise les deux cœurs. Que ce soit le cas ou non, cela n'a aucun effet sur la détérioration de l'utilisation globale du processeur au fil du temps.
  • Lorsque je regarde les threads dans VisualVM, tous les threads de pool sont en cours d'exécution, aucun n'est en attente ou autre.
  • Selon VisualVM, 90% du temps CPU est consacré à un appel de fonction native (résolution d'un petit programme linéaire)
  • Le garbage collection n'est pas un problème car l'application ne crée pas et ne dé-référence pas beaucoup d'objets. En outre, la plupart de la mémoire semble être allouée hors du tas. 4g de tas suffisent sous Linux et 8g sous Windows pour la plus grande instance.

Ce que j'ai essayé

  • toutes sortes d'arguments JVM, XMS élevé, métaspace élevé, drapeau UseNUMA, autres GC.
  • différentes JVM (Hotspot 8, 9, 10, 11).
  • différentes bibliothèques natives de différents solveurs de programmation linéaire (CLP, Xpress, Cplex, Gurobi).

Des questions

  • Qu'est-ce qui explique la différence de performances entre Linux et Windows d'une grande application Java multi-thread qui fait un usage intensif des appels natifs?
  • Y a-t-il quelque chose que je puisse changer dans l'implémentation qui aiderait Windows, par exemple, devrais-je éviter d'utiliser un ExecutorService qui reçoit des milliers de Callables et faire quoi à la place?
Nils
la source
Avez-vous essayé ForkJoinPoolau lieu de ExecutorService? 25% d'utilisation du processeur est vraiment faible si votre problème est lié au processeur.
Karol Dowbecki
1
Votre problème ressemble à quelque chose qui devrait pousser le processeur à 100% et pourtant vous êtes à 25%. Pour certains problèmes, ForkJoinPoolc'est plus efficace que la planification manuelle.
Karol Dowbecki
2
En parcourant les versions de Hotspot, vous êtes-vous assuré d'utiliser la version "serveur" et non "client"? Quelle est votre utilisation du processeur sous Linux? De plus, la disponibilité de Windows de plusieurs jours est impressionnante! Quel est ton secret? : P
erickson
3
Essayez peut-être d'utiliser Xperf pour générer un FlameGraph . Cela pourrait vous donner un aperçu de ce que fait le processeur (espérons-le à la fois en mode utilisateur et en mode noyau), mais je ne l'ai jamais fait sous Windows.
Karol Dowbecki
1
@Nils, les deux exécutions (unix / win) utilisent la même interface pour appeler la bibliothèque native? Je demande, car ça a l'air différent. Comme: win utilise jna, linux jni.
SR

Réponses:

2

Pour Windows, le nombre de threads par processus est limité par l'espace d'adressage du processus (voir également Mark Russinovich - Repousser les limites de Windows: processus et threads ). Pensez que cela provoque des effets secondaires quand il approche des limites (ralentissement des changements de contexte, fragmentation ...). Pour Windows, j'essayerais de diviser la charge de travail en un ensemble de processus. Pour un problème similaire que j'avais il y a des années, j'ai implémenté une bibliothèque Java pour le faire plus facilement (Java 8), jetez un œil si vous le souhaitez: Bibliothèque pour générer des tâches dans un processus externe .

geri
la source
Cela semble très intéressant! J'hésite un peu à aller jusque-là (encore) pour deux raisons: 1) il y aura une surcharge de performances de sérialisation et d'envoi d'objets via des sockets; 2) si je veux tout sérialiser, cela inclut toutes les dépendances liées dans une tâche - ce serait un peu de travail de réécrire le code - néanmoins, merci pour le ou les liens utiles.
Nils
Je partage pleinement vos préoccupations et la refonte du code serait un effort. En parcourant le graphique, vous devez introduire un seuil pour le nombre de threads quand il est temps de diviser le travail en un nouveau sous-processus. Pour aborder 2) jetez un œil au fichier mappé en mémoire Java (java.nio.MappedByteBuffer), avec lequel vous pouvez partager efficacement les données entre les processus, par exemple vos données de graphique. Godspeed :)
geri
0

On dirait que Windows met en cache de la mémoire dans le fichier d'échange, après avoir été intact pendant un certain temps, et c'est pourquoi le processeur est goulot d'étranglement par la vitesse du disque

Vous pouvez le vérifier avec Process Explorer et vérifier la quantité de mémoire mise en cache

Juif
la source
Tu penses? Il y a suffisamment de mémoire libre. Pourquoi Windows commencerait-il à échanger? Quoi qu'il en soit, merci.
Nils
Au moins sur les fenêtres de mon ordinateur portable, on échange des applications parfois minimisées, même avec suffisamment de mémoire
Juif
0

Je pense que cette différence de performances est due à la façon dont le système d'exploitation gère les threads. JVM cache toute différence de système d'exploitation. Il existe de nombreux sites où vous pouvez en lire plus, comme celui-ci , par exemple. Mais cela ne signifie pas que la différence disparaît.

Je suppose que vous utilisez JVM Java 8+. Pour cette raison, je vous suggère d'essayer d'utiliser les fonctionnalités de programmation en flux et fonctionnelles. La programmation fonctionnelle est très utile lorsque vous avez de nombreux petits problèmes indépendants et que vous souhaitez passer facilement d'une exécution séquentielle à une exécution parallèle. La bonne nouvelle est que vous n'avez pas à définir de stratégie pour déterminer le nombre de threads à gérer (comme avec ExecutorService). Juste par exemple (tiré d' ici ):

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

}

Résultat:

Pour les flux normaux, cela prend 1 minute 10 secondes. Pour les flux parallèles, cela prend 23 secondes. PS testé avec i7-7700, 16G RAM, WIndows 10

Donc, je vous suggère de lire sur la programmation des fonctions, le flux, la fonction lambda en Java et d'essayer d'implémenter un petit nombre de tests avec votre code (adapté pour fonctionner dans ce nouveau contexte).

xcesco
la source
J'utilise des flux dans d'autres parties du logiciel, mais dans ce cas, les tâches sont créées lors de la traversée d'un graphique. Je ne saurais pas comment envelopper cela en utilisant des flux.
Nils
Pouvez-vous parcourir le graphique, créer une liste puis utiliser des flux?
xcesco
Les flux parallèles ne sont que du sucre syntaxique pour un ForkJoinPool. J'ai essayé (voir le commentaire @KarolDowbecki ci-dessus).
Nils
0

Souhaitez-vous s'il vous plaît publier les statistiques du système? Le gestionnaire de tâches est assez bon pour fournir des indices si c'est le seul outil disponible. Il peut facilement dire si vos tâches attendent des E / S - ce qui ressemble au coupable en fonction de ce que vous avez décrit. Cela peut être dû à un problème de gestion de la mémoire, ou la bibliothèque peut écrire des données temporaires sur le disque, etc.

Lorsque vous parlez de 25% d'utilisation du processeur, voulez-vous dire que seuls quelques cœurs sont occupés à travailler en même temps? (Il se peut que tous les cœurs fonctionnent de temps en temps, mais pas simultanément.) Vérifiez-vous combien de threads (ou processus) sont réellement créés dans le système? Le nombre est-il toujours supérieur au nombre de cœurs?

S'il y a suffisamment de fils, nombre d'entre eux attendent-ils quelque chose? Si vrai, vous pouvez essayer d'interrompre (ou joindre un débogueur) pour voir ce qu'ils attendent.

Xiao-Feng Li
la source
J'ai ajouté une capture d'écran du gestionnaire de tâches pour une exécution représentative de ce problème. L'application elle-même crée autant de threads qu'il y a de cœurs physiques sur la machine. Java contribue un peu plus de 50 threads à ce chiffre. Comme déjà dit, VisualVM indique que tous les threads sont occupés (vert). Ils ne poussent tout simplement pas le processeur à la limite sous Windows. Ils le font sur Linux.
Nils
@Nils Je suppose que vous n'avez pas vraiment tous les fils occupés en même temps, mais en fait seulement 9 à 10 d'entre eux. Ils sont programmés de manière aléatoire sur tous les cœurs, d'où une utilisation moyenne de 9/44 = 20%. Pouvez-vous utiliser des threads Java directement plutôt que ExecutorService pour voir la différence? Il n'est pas difficile de créer 44 threads, et chacun saisissant Runnable / Callable à partir d'un pool de tâches / file d'attente. (Bien que VisualVM montre que tous les threads Java sont occupés, la réalité peut être que les 44 threads sont planifiés rapidement afin que tous aient une chance de s'exécuter dans la période d'échantillonnage de VisualVM.)
Xiao-Feng Li
C'est une pensée et quelque chose que j'ai fait à un moment donné. Dans mon implémentation, je me suis également assuré que l'accès natif est local à chaque thread, mais cela n'a fait aucune différence.
Nils