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
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.
A voir également:

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
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
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.
1
Jestiz_ Messages postés 34 Date d'inscription vendredi 31 août 2018 Statut Membre Dernière intervention 18 décembre 2018
20 sept. 2018 à 15:33
Très bien merci de l'info.

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 ?
0
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
20 sept. 2018 à 18:20
Si c'est une tâche périodique il faudrait regarder les Executors.

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.
0
Jestiz_ Messages postés 34 Date d'inscription vendredi 31 août 2018 Statut Membre Dernière intervention 18 décembre 2018
20 sept. 2018 à 18:48
Je reprends de 0 pour être le plus clair possible, il y a disons 1000 fours, 1 four a environ 10 actions à faire toutes les 0.05s.

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.
0
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
20 sept. 2018 à 19:49
Ce n'est pas que je ne comprends pas, c'est surtout que l'on pourrait interpréter ton problème de plusieurs manières et que le code serait donc différent pour l'un ou l'autre... et le résultat aussi.

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 :-)
0
Jestiz_ Messages postés 34 Date d'inscription vendredi 31 août 2018 Statut Membre Dernière intervention 18 décembre 2018
Modifié le 20 sept. 2018 à 20:24
L'ordre des actions n'est pas du tout important elles peuvent être faites en même temps. Et pour ma part je voyais plus le code se réaliser quoiqu'il arrive (même si le four 2 n'a pas terminé on lance quand même les actions). Après je me pose la question si à un moment il n'y a pas 2 fours qui vont interférer en même temps.

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.
0
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
J'ai finalement opté pour séparer les tâches en nombre max de fours dedans, mon code est comme ceci :

		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
0
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
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
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();
}
0
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
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 ?
0
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
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.

.collect(Collectors.toCollection(() -> new ArrayList<>(furnaces.size())));
0
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
Wow merci beaucoup, j'ai appris beaucoup de choses sur ce thread, je pensais pas qu'on pouvait encore plus optimiser !
0