web-dev-qa-db-fra.com

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

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. Comme il y a plus de sous-problèmes que de cœurs de processeur, j'utilise un ExecutorService et je définis 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 d'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:

Task Manager CPU utilization

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

Questions

  • Qu'est-ce qui explique la différence de performances entre Linux et Windows d'une grande application multi-thread Java 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?
18
Nils

Pour Windows, le nombre de threads par processus est limité par l'espace d'adressage du processus (voir aussi Mark Russinovich - Repousser les limites de Windows: processus et threads ). Pensez que cela provoque des effets secondaires quand il se rapproche 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 .

2
geri

On dirait que Windows met en cache de la mémoire dans le fichier d'échange, après qu'il n'a pas été modifié 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

0
Jew

Je pense que cette différence de performance est due à la façon dont l'O.S. 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 this , par exemple. Mais cela ne signifie pas que la différence disparaît.

Je suppose que vous exécutez sur Java 8+ JVM. Pour cette raison, je vous suggère d'essayer d'utiliser des 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 vous souhaitez passer facilement de l'exécution séquentielle à l'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 que vous devez gérer (comme avec ExecutorService). Par exemple (tiré de - 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. P.S Testé avec i7-7700, 16G RAM, WIndows 10

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

0
xcesco

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 IO - ce qui semble être le coupable en fonction de ce que vous avez décrit. Cela peut être dû à un certain problème de gestion de la mémoire, ou la bibliothèque peut écrire des données temporaires sur le disque, etc.

Lorsque vous dites 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 threads, 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.

0
Xiao-Feng Li