Performances des tâches (runnables)
Fermé
Jestiz_
Messages postés
34
Date d'inscription
vendredi 31 août 2018
Statut
Membre
Dernière intervention
18 décembre 2018
-
19 sept. 2018 à 21:54
Jestiz_ Messages postés 34 Date d'inscription vendredi 31 août 2018 Statut Membre Dernière intervention 18 décembre 2018 - 22 sept. 2018 à 20:34
Jestiz_ Messages postés 34 Date d'inscription vendredi 31 août 2018 Statut Membre Dernière intervention 18 décembre 2018 - 22 sept. 2018 à 20:34
A voir également:
- Performances des tâches (runnables)
- Windows 11 barre des taches a gauche - Guide
- Tester les performances de son pc - Guide
- Comment remettre la barre des tâches à la normale ? - Guide
- Gestionnaire des taches - Guide
- Barre des taches windows 11 - Accueil - Interface
2 réponses
KX
Messages postés
16752
Date d'inscription
samedi 31 mai 2008
Statut
Modérateur
Dernière intervention
31 août 2024
3 019
19 sept. 2018 à 22:29
19 sept. 2018 à 22:29
Bonjour,
En soit un Runnable ne va rien faire, c'est juste un point d'entrée qui permet de décrire du code qui devra être exécuté (mais sans l'exécuter).
Les performances dépendront de comment le Runnable est appelé, en particulier c'est souvent utilisé lorsque l'on créé de nouveaux Thread, qui tournent en parallèle.
Si tu as une machine avec un processeur "quad-core" (avec 4 cœurs), ton ordinateur peut traiter 4 opérations en même temps, si tu traites 1000 Runnable à la suite, tu vas utiliser 1 seul cœur pendant 1000 T (le temps d'un Runnable), alors que si tu parallélise avec 4 threads, tu utiliserais les 4 cœurs pendant 250 T, ce sera donc 4 fois plus rapide.
Remarque : il serait contre-productif d'avoir 1000 threads, car la mise en place d'un thread est quand même un peu coûteuse, alors que le nombre de cœurs est de toute façon limité.
Depuis Java 8, les ParallelStream permettent de ne plus trop se poser la question, Java va automatiquement détecter la configuration de l'ordinateur et créer autant de threads que de cœurs disponibles.
Exemple, si tu as une
En soit un Runnable ne va rien faire, c'est juste un point d'entrée qui permet de décrire du code qui devra être exécuté (mais sans l'exécuter).
Les performances dépendront de comment le Runnable est appelé, en particulier c'est souvent utilisé lorsque l'on créé de nouveaux Thread, qui tournent en parallèle.
Si tu as une machine avec un processeur "quad-core" (avec 4 cœurs), ton ordinateur peut traiter 4 opérations en même temps, si tu traites 1000 Runnable à la suite, tu vas utiliser 1 seul cœur pendant 1000 T (le temps d'un Runnable), alors que si tu parallélise avec 4 threads, tu utiliserais les 4 cœurs pendant 250 T, ce sera donc 4 fois plus rapide.
Remarque : il serait contre-productif d'avoir 1000 threads, car la mise en place d'un thread est quand même un peu coûteuse, alors que le nombre de cœurs est de toute façon limité.
Depuis Java 8, les ParallelStream permettent de ne plus trop se poser la question, Java va automatiquement détecter la configuration de l'ordinateur et créer autant de threads que de cœurs disponibles.
Exemple, si tu as une
List<Runnable>avec tes 1000 actions à faire, tu peux faire
list.forEach(Runnable::run);sur un seul cœur, ou
list.stream().parallel().forEach(Runnable::run);en parallèle.
Jestiz_
Messages postés
34
Date d'inscription
vendredi 31 août 2018
Statut
Membre
Dernière intervention
18 décembre 2018
21 sept. 2018 à 21:23
21 sept. 2018 à 21:23
J'ai finalement opté pour séparer les tâches en nombre max de fours dedans, mon code est comme ceci :
Je me demande comment mettre tout ça pour que tous les coeurs soient utilisés et comment éviter d'itérer au travers de la liste 2 fois (removeIf et forEach). Faut-il utiliser une autre méthode que les streams ? Merci
private Set<Furnace> furnaces = new HashSet<>(); @Override public void run() { furnaces.removeIf(furnace -> furnace.getBurnTime() < 1 || furnace.getLocation().getBlock().getType() != Material.BURNING_FURNACE); furnaces.stream().parallel().forEach(furnace -> { furnace.setCookTime((short) (furnace.getCookTime()+options.getSmeltMultiplier())); furnace.setBurnTime((short) Math.max(1, furnace.getBurnTime()-options.getFurnaceConsumingMultiplier())); furnace.update(); }); if(furnaces.isEmpty()) cancel(); }
Je me demande comment mettre tout ça pour que tous les coeurs soient utilisés et comment éviter d'itérer au travers de la liste 2 fois (removeIf et forEach). Faut-il utiliser une autre méthode que les streams ? Merci
KX
Messages postés
16752
Date d'inscription
samedi 31 mai 2008
Statut
Modérateur
Dernière intervention
31 août 2024
3 019
22 sept. 2018 à 00:34
22 sept. 2018 à 00:34
Pour mettre le removeIf dans le stream il faudrait utiliser la méthode filter (en inversant la condition).
Mais pour l'utilisation en parallèle c'est déjà bon, c'est ce que fait la méthode parallel()
Pour que le hashSet soit performant il faudrait
1) que tu lui alloues une capacitésuffisante au départ
2) que les méthodes hashCode et equals soient le plus rapide et discriminante possible.
Mais dans ton cas, une List devrait être plus performante (si on enlève le removeIf).
Quant au short, ça prends un peu moins de place en mémoire, mais on l'utilise plutôt sur du stockage de données (dans un fichier par exemple), pour des calculs int ou long sont à privilégier.
Pour encore accélérer un peu le programme, il faudrait aussi calculer une seule fois les valeurs qui ne changent jamais, par exemple celles des options.
On pourrait aussi s'éviter le double appel de méthode set(get()+n) avec une méthode add(n)
Remarque : vu ton Math.max, je ne vois pas comment tu pourrais avoir furnace -> furnace.getBurnTime() < 1 dans ton removeIf... je pense qu'il faut enlever l'un des deux.
Mais pour l'utilisation en parallèle c'est déjà bon, c'est ce que fait la méthode parallel()
Pour que le hashSet soit performant il faudrait
1) que tu lui alloues une capacitésuffisante au départ
furnaces = new HashSet<>(1000);
2) que les méthodes hashCode et equals soient le plus rapide et discriminante possible.
Mais dans ton cas, une List devrait être plus performante (si on enlève le removeIf).
Quant au short, ça prends un peu moins de place en mémoire, mais on l'utilise plutôt sur du stockage de données (dans un fichier par exemple), pour des calculs int ou long sont à privilégier.
Pour encore accélérer un peu le programme, il faudrait aussi calculer une seule fois les valeurs qui ne changent jamais, par exemple celles des options.
On pourrait aussi s'éviter le double appel de méthode set(get()+n) avec une méthode add(n)
Remarque : vu ton Math.max, je ne vois pas comment tu pourrais avoir furnace -> furnace.getBurnTime() < 1 dans ton removeIf... je pense qu'il faut enlever l'un des deux.
private List<Furnace> furnaces; @Override public void run() { int smeltMultiplier = options.getSmeltMultiplier(); int furnaceConsumingMultiplier = options.getFurnaceConsumingMultiplier(); furnaces = furnaces.stream() .parallel() .filter(furnace -> furnace.getBurnTime() >= 1) .filter(furnace -> furnace.getLocation().getBlock().getType() == Material.BURNING_FURNACE) .map(furnace -> { furnace.addCookTime(smeltMultiplier); // cooktime += smeltMultiplier furnace.reduceBurnTime(furnaceConsumingMultiplier); // burnTime -= furnaceConsumingMultiplier; if (furnaceConsumingMultiplier < 1) furnaceConsumingMultiplier = 1; furnace.update(); return furnace; }) .collect(Collectors.toList()); if (furnaces.isEmpty()) cancel(); }
Jestiz_
Messages postés
34
Date d'inscription
vendredi 31 août 2018
Statut
Membre
Dernière intervention
18 décembre 2018
22 sept. 2018 à 18:34
22 sept. 2018 à 18:34
J'ai appliqué tes conseils et c'est vrai que je ne pensais pas à tant d'optimisation, bonne idée, le seul truc que je ne comprends pas est pourquoi utiliser la fonction map au lieu de forEach ?
KX
Messages postés
16752
Date d'inscription
samedi 31 mai 2008
Statut
Modérateur
Dernière intervention
31 août 2024
3 019
>
Jestiz_
Messages postés
34
Date d'inscription
vendredi 31 août 2018
Statut
Membre
Dernière intervention
18 décembre 2018
22 sept. 2018 à 18:52
22 sept. 2018 à 18:52
Le forEach est une opération terminale, on ne pourrait donc pas enchaîner avec un collect derrière.
Avec ta proposition, tu avais un seul Set et tu supprimais des données au fur et à mesure dedans, sauf que supprimer des données c'est coûteux, il est moins coûteux (en temps) de copier partiellement une List dans une autre (inconvénient : ça prend un peu plus de place en mémoire d'avoir deux collections en même temps).
Remarque : pour optimiser encore un peu, il vaudrait mieux spécifier une taille initiale à ta liste de résultat, sinon il va passer son temps à la redimensionner.
Avec ta proposition, tu avais un seul Set et tu supprimais des données au fur et à mesure dedans, sauf que supprimer des données c'est coûteux, il est moins coûteux (en temps) de copier partiellement une List dans une autre (inconvénient : ça prend un peu plus de place en mémoire d'avoir deux collections en même temps).
Remarque : pour optimiser encore un peu, il vaudrait mieux spécifier une taille initiale à ta liste de résultat, sinon il va passer son temps à la redimensionner.
.collect(Collectors.toCollection(() -> new ArrayList<>(furnaces.size())));
Jestiz_
Messages postés
34
Date d'inscription
vendredi 31 août 2018
Statut
Membre
Dernière intervention
18 décembre 2018
>
KX
Messages postés
16752
Date d'inscription
samedi 31 mai 2008
Statut
Modérateur
Dernière intervention
31 août 2024
22 sept. 2018 à 20:34
22 sept. 2018 à 20:34
Wow merci beaucoup, j'ai appris beaucoup de choses sur ce thread, je pensais pas qu'on pouvait encore plus optimiser !
20 sept. 2018 à 15:33
Mais j'ai oublié de préciser que ces tâches sont répétitives, dans mon cas elles s'exécuteront tous les 1 tick
(20 ticks = 1s et 1 tick = 0.05s) pendant par exemple 10 secondes.
Je ferais donc mieux de mettre une limite d'objets dans une tâche par exemple 250/tâches et les appeler avec la méthode parallel pour commencer leur exécution ?
C'est la méthode qui me semble la plus adaptée non ?
20 sept. 2018 à 18:20
Est-ce que c'est 1 tâche de 10s qui s'exécute toutes les 0.05s (soit 200 en parallèles), ou pour chacune "tick" tu déclenches les 1000 d'un coup (soit 200 000 ?), si tu pouvais préciser exactement ce que tu veux, je pourrais t'orienter de manière plus adaptée, pour l'instant c'est encore pas mal flou.
Là encore, Java est largement capable de gérer au mieux ses ressources, sans que ce soit à toi de faire le découpage des tâches, mais il faudrait effectivement être précis dans la manière d'écrire le code pour gérer les performances.
20 sept. 2018 à 18:48
Je me demandais donc s'il fallait les répartir avec un plafond de 250 fours par tâche répétitives, soit 4 tâches dans notre cas.
Et chaque 0.05s, les 10 actions sont exécutées(depuis la méthode run) jusqu'à un certain temps inconnu du programme (c'est en fonction de ce que le joueur utilise comme carburant, ça peut aller de 1 à ~100 secondes).
Mais quoi qu'il arrive, les tâches s'exécuteront toujours toutes les 0.05s.
Si c'est pas assez clair dit moi exactement car moi non plus je ne comprends pas ce que tu ne comprends pas haha.
20 sept. 2018 à 19:49
Prenons 1 four pour l'instant :
À t=0, on fait 10 actions sur ce four, disons que ça termine à t=0.02s
À t=0.05s on fait 10 actions, ça se termine à t=0.07s
À t=0.10s on fait 10 actions, ça se termine à t=0.12s, etc.
OU
À t=0, on fait 10 actions sur ce four, ça termine à t=0.02s, on attend 0.05s
À t=0.07s on fait 10 actions, ça se termine à t=0.09s, on attend 0.05s
À t=0.14s on fait 10 actions, ça se termine à t=0.16s, on attend 0.05s, etc.
Hypothèse :
À t=0, on lance 10 actions sur le four, mais cela prends 0.07s, que faire ?
À t=0.05s on lance quand même 10 actions sur le four
À t=0.07s les 10 premières actions se terminent
À t=0.10s on lance 10 nouvelles actions
À t=0.12s les 10 actions suivantes se terminent, etc.
OU
On attends t=0.07s pour lancer les 10 actions suivantes.
À t=0.07s les 10 premières actions se terminent et on en lance 10 nouvelles.
À t=0.14s les 10 actions suivantes se terminent et on en lance 10 nouvelles, etc.
Question :
Les 10 actions doivent elles être faites dans l'ordre ou elle peuvent être faites en même temps ?
Maintenant avec 2 fours :
À t=0, on lance 10 actions sur le four 1, et 10 actions sur le four 2.
Hypothèse :
À t=0.05s les actions sur le four 1 sont terminées, mais pas celles sur le four 2, que faire ?
On attends que les actions sur tous les fours soient terminées avant de relancer les 20 actions.
OU
On lance les nouvelles actions sur le four 1 mais on attend que le four 2 ait terminé avant de lancer ses actions.
OU
On lance les nouvelles actions sur les 2 fours, en plus des actions sur le four 2 qui ne sont pas terminées.
Bref, c'est ce genre de questions qui se posent :-)
Modifié le 20 sept. 2018 à 20:24
Enfaite avant je ne m'étais jamais vraiment soucié de tout ça, je procédais comme ceci : une LinkedList présente dans une seule tâche contenait tous les fours. Au moment d'appeler run(), un itérateur parcourait la liste et me sortait disons un objet four, sur ce dernier j'exécutais différentes actions puis je passais au suivant etc.
Je cherche donc maintenant à améliorer ce code, à le rendre plus performant. C'est pour ça que je me pose un tas de questions.