web-dev-qa-db-fra.com

Les instructions conditionnelles non triviales doivent-elles être déplacées vers la section d'initialisation des boucles?

J'ai eu cette idée de cette question sur stackoverflow.com

Le modèle suivant est courant:

final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
  //...do something
}

Le point que j'essaie de faire est que l'énoncé conditionnel est quelque chose de compliqué et ne change pas.

Est-il préférable de le déclarer dans la section d'initialisation de la boucle, en tant que tel?

final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
  //...do something
}

Est-ce plus clair?

Que faire si l'expression conditionnelle est simple, comme

final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
  //...do something
}
21
Celeritas

Ce que je ferais, c'est quelque chose comme ça:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

Honnêtement, la seule bonne raison de cram initialiser j (maintenant limit) dans l'en-tête de boucle est de le garder correctement délimité. Tout ce qu'il faut pour faire en sorte qu'un non-problème soit une portée de fermeture étanche de Nice.

Je peux apprécier le désir d'être rapide mais ne sacrifie pas la lisibilité sans une vraie bonne raison.

Bien sûr, le compilateur peut être optimisé, l'initialisation de plusieurs variables peut être légale, mais les boucles sont suffisamment difficiles à déboguer telles quelles. Veuillez être gentil avec les humains. Si cela nous ralentit vraiment, c'est bien de le comprendre suffisamment pour le réparer.

62
candied_orange

Un bon compilateur générera le même code de toute façon, donc si vous recherchez des performances, n'effectuez un changement que s'il se trouve dans une boucle critique et vous l'ont en fait profilé et trouvé que cela fait une différence. Même si le compilateur ne peut pas l'optimiser, comme les gens l'ont souligné dans les commentaires sur le cas des appels de fonction, dans la grande majorité des situations, la différence de performances va être trop petite pour mériter la considération d'un programmeur.

Cependant ...

Nous ne devons pas oublier que le code est principalement un moyen de communication entre les humains, et vos deux options ne communiquent pas très bien aux autres humains. La première donne l'impression que l'expression doit être calculée à chaque itération, et la seconde étant dans la section d'initialisation implique qu'elle sera mise à jour quelque part dans la boucle, où elle est vraiment constante tout au long.

Je préférerais en fait qu'il soit retiré au-dessus de la boucle et rendu final pour que cela soit immédiatement et abondamment clair pour quiconque lit le code. Ce n'est pas idéal non plus car cela augmente la portée de la variable, mais votre fonction englobante ne devrait pas contenir beaucoup plus que cette boucle de toute façon.

38
Karl Bielefeldt

Comme l'a dit @Karl Bielefeldt dans sa réponse, ce n'est généralement pas un problème.

Cependant, c'était à un moment un problème commun en C et C++, et une astuce est survenue pour contourner le problème sans réduire la lisibilité du code— itérer en arrière, jusqu'à 0.

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

Maintenant, le conditionnel dans chaque itération est juste >= 0 Que chaque compilateur compilera en 1 ou 2 instructions d'assemblage. Chaque CPU fabriqué au cours des dernières décennies devrait avoir des vérifications de base comme celles-ci; en faisant une vérification rapide sur ma machine x64, je vois que cela se transforme de manière prévisible en cmpl $0x0, -0x14(%rbp) (valeur de comparaison longue-int 0 vs registre rbp décalé -14) et jl 0x100000f59 (passez à l'instruction suivante la boucle si la comparaison précédente était vraie pour "2nd-arg <1st-arg") .

Notez que j'ai supprimé le + 1 De Math.floor(Math.sqrt(x)) + 1; pour que les mathématiques fonctionnent, la valeur de départ doit être int i = «iterationCount» - 1. Il convient également de noter que votre itérateur doit être signé; unsigned int Ne fonctionnera pas et avertira probablement le compilateur.

Après avoir programmé dans des langages basés sur C pendant environ 20 ans, je n'écris maintenant que des boucles d'itération d'index inversé, sauf s'il y a une raison spécifique pour effectuer une itération d'index vers l'avant. En plus des vérifications plus simples dans les conditions, l'itération inversée passe souvent également à côté de ce qui serait autrement des mutations de tableau gênantes pendant l'itération.

9
Slipp D. Thompson

Cela devient intéressant une fois que Math.sqrt (x) est remplacé par Mymodule.SomeNonPureMethodWithSideEffects (x).

Fondamentalement, mon modus operandi est le suivant: si quelque chose doit toujours donner la même valeur, alors ne l'évaluez qu'une seule fois. Par exemple, List.Count, si la liste est censée ne pas changer pendant le fonctionnement de la boucle, alors obtenez le nombre en dehors de la boucle dans une autre variable.

Certains de ces "décomptes" peuvent être étonnamment coûteux, en particulier lorsque vous traitez avec des bases de données. Même si vous travaillez sur un ensemble de données qui n'est pas censé changer lors de l'itération de la liste.

3
Pieter B

À mon avis, c'est très spécifique à la langue. Par exemple, si j'utilise C++ 11, je soupçonne que si la vérification de condition était une fonction constexpr, le compilateur est très susceptible d'optimiser les multiples exécutions car il sait qu'il produira la même valeur à chaque fois.

Cependant, si l'appel de fonction est une fonction de bibliothèque qui n'est pas constexpr, le compilateur va presque certainement l'exécuter à chaque itération car il ne peut pas le déduire (sauf s'il est en ligne et peut donc être déduit comme pur).

Je sais moins de choses sur Java mais étant donné que JIT est compilé, je suppose que le compilateur a suffisamment d'informations à l'exécution pour probablement aligner et optimiser la condition. Mais cela dépendrait d'une bonne conception du compilateur et du compilateur décider de cette boucle était une priorité d'optimisation que nous ne pouvons que deviner.

Personnellement, je pense qu'il est légèrement plus élégant de mettre la condition à l'intérieur de la boucle for si vous le pouvez, mais si c'est complexe, je l'écrirai dans une fonction constexpr ou inline, ou vos langues équivalentes pour indiquer que la fonction est pure et optimisable. Cela rend l'intention évidente et maintient le style de boucle idiomatique sans créer une énorme ligne illisible. Il donne également à la vérification de condition un nom s'il s'agit de sa propre fonction afin que les lecteurs puissent immédiatement voir de façon logique à quoi sert la vérification sans la lire si elle est complexe.

0
Vality