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:
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?
ForkJoinPool
c'est plus efficace que la planification manuelle.
ForkJoinPool
au lieu deExecutorService
? 25% d'utilisation du processeur est vraiment faible si votre problème est lié au processeur.