Performances des tâches (runnables)
Jestiz_
Messages postés
34
Date d'inscription
Statut
Membre
Dernière intervention
-
Jestiz_ Messages postés 34 Date d'inscription Statut Membre Dernière intervention -
Jestiz_ Messages postés 34 Date d'inscription Statut Membre Dernière intervention -
Bonjour, je me pose la question, est-ce que les tâches ou runnables sont coûteuse en performance ? Je m'explique :
Dans un programme pour minecraft je me retrouve à gérer disons 1000 fourneaux (vitesse de consommation des carburants, vitesse de cuisson des ingrédients et d'autres actions diverses) et je me demandais ce qui est le mieux en terme de performance; une seule tâche avec une boucle en utilisant un itérateur pour parcourir une liste où sons stockés ces fours (et exécuter tous les "calculs" nécessaires) ou bien une seule tâche pour 1, 2 voir même 10 fours.
J'espère avoir été le plus clair possible. Cordialement.
Dans un programme pour minecraft je me retrouve à gérer disons 1000 fourneaux (vitesse de consommation des carburants, vitesse de cuisson des ingrédients et d'autres actions diverses) et je me demandais ce qui est le mieux en terme de performance; une seule tâche avec une boucle en utilisant un itérateur pour parcourir une liste où sons stockés ces fours (et exécuter tous les "calculs" nécessaires) ou bien une seule tâche pour 1, 2 voir même 10 fours.
J'espère avoir été le plus clair possible. Cordialement.
A voir également:
- Performances des tâches (runnables)
- Windows 11 barre des taches a gauche - Guide
- Tester les performances de son pc - Guide
- Barre des taches - Guide
- Gestionnaire des taches windows 11 - Guide
- Gestionnaire des taches - Guide
2 réponses
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.
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
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(); }
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())));
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 ?
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.
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.
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 :-)
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.