Dans quelle mesure l’ordre des étiquettes de cas affecte-t-il l’efficacité des instructions switch?

Considérer:

if (condition1) { // Code block 1 } else { // Code block 2 } 

Si je sais que la condition1 sera true la plupart du temps, je devrais alors coder la logique comme suit, au lieu de:

 if (!condition1) { // Code block 2 } else { // Code block 1 } 

puisque je vais éviter la pénalité du jump au deuxième bloc de code (note: j’ai une connaissance limitée du langage assembleur). Est-ce que cette idée est rescope pour switch instructions et les étiquettes de case ?

 switch (myCaseValue) { case Case1: // Code block 1 break; case Case2: // Code block 2 break; // etc. } 

Si je sais que l’un des cas se produira plus souvent, puis-je réorganiser l’ordre des étiquettes de case afin qu’il soit plus efficace? Devrais-je? Dans mon code, j’ai commandé les étiquettes de cas par ordre alphabétique pour assurer la lisibilité du code sans vraiment y penser. Est-ce que cette micro-optimisation?

Quelques faits pour le matériel moderne comme x86 ou x86_64:

  • Une twig prise sans condition n’entraîne presque aucun coût supplémentaire, à part le décodage. Si vous voulez un nombre, c’est environ un quart d’heure.
  • Une twig conditionnelle, qui a été correctement prédite, n’entraîne pratiquement aucun coût supplémentaire.
  • Une twig conditionnelle, qui n’a pas été correctement prédite, a une pénalité égale à la longueur des pipelines du processeur, soit environ 12 à 20 horloges, en fonction du matériel.
  • Les mécanismes de prédiction sont très sophistiqués. Les boucles avec un faible nombre d’itérations (sur le Core 2 par exemple jusqu’à 64) peuvent être parfaitement prédites. On peut prédire de petits motifs répétitifs tels que “pris-pris-non pris-pris” s’ils ne sont pas trop longs (IIRC 6 sur Core2).

Vous pouvez en savoir plus sur la prédiction de twig dans Agner Fogs excellent manual.

Les instructions de commutateur sont généralement remplacées par une table de saut par le compilateur. Dans la plupart des cas, l’ordre des affaires ne fera aucune différence. Il existe également des mécanismes de prévision pour les sauts indirects.

La question n’est donc pas de savoir si vos sauts sont plus susceptibles d’être effectués, mais bien s’ils sont bien prévisibles, du moins pour le matériel sur lequel vous souhaitez exécuter votre code. Ce n’est pas une question facile du tout. Mais si vous avez des twigs qui dépendent d’une condition aléatoire (ou pseudo-aléatoire), vous pouvez essayer de la reformuler en une instruction sans twig si possible.

Votre conclusion concernant les déclarations if ne sera pas vraie pour la plupart des matériels que je connais bien. Le problème n’est pas que vous sautez, mais que vous twigz. Le code peut aller de deux manières différentes, en fonction du résultat d’une comparaison. Cela peut bloquer le pipeline sur la plupart des processeurs modernes. La prédiction de twig est courante et résout le problème la plupart du temps, mais n’a rien à voir avec votre exemple. Le prédicteur peut également prédire qu’une comparaison sera fausse, tout comme elle peut être vraie.

Comme d’habitude, voir wikipedia: Prédicteur de twig

Ça dépend. Le compilateur utilisera un ensemble de critères internes dépendant de l’implémentation pour décider d’implémenter le switch sous la forme d’une séquence de tests de type if ou sous la forme d’une table de saut. Cela peut dépendre, par exemple, de la “compression” de votre jeu d’étiquettes de case . Si vos valeurs d’étiquette de case forment un ensemble “dense”, le compilateur utilisera probablement plus de table de sauts, auquel cas la commande des étiquettes de case n’aura pas d’importance. S’il décide d’aller avec ce qui ressemble à une séquence de tests if-else, l’ordre peut avoir de l’importance.

Gardez toutefois à l’esprit que le corps du switch est une déclaration de grande taille, avec des libellés de case fournissant plusieurs points d’entrée dans cette déclaration. Pour cette raison, la capacité du compilateur (ainsi que du vôtre) de réorganiser le case “sous-blocs” dans cette instruction pourrait être limitée.

Les étiquettes de cas doivent être commandées de la manière la plus efficace pour la lisibilité.

Réordonner les étiquettes de cas pour plus d’efficacité est un cas d’optimisation prématurée, sauf si un profileur vous a spécifiquement dit que c’était un problème.

Je pense que même votre prémisse initiale – que vous pouvez optimiser la déclaration if en réorganisant le conditionnel peut très bien être erronée. Dans une version non optimisée, vous constaterez peut-être que faire ce dont vous parlez a une certaine valeur – peut-être. Dans le cas général, vous devrez sauter au moins une fois dans l’un ou l’autre cas, il n’y a donc aucun avantage (en général) à organiser le conditionnel de toute façon. Mais c’est pour les builds non optimisés, alors qui se soucie de cette optimisation?

Dans les versions optimisées, je pense que vous pourriez être surpris par ce qu’un compilateur génère parfois pour une instruction if. Le compilateur peut déplacer l’un ou l’autre cas (ou les deux) vers un emplacement hors ligne. Je pense que vous essayez d’optimiser cela naïvement en jouant avec la condition «vient en premier» ne fera pas nécessairement ce que vous voulez. Au mieux, vous ne devriez le faire qu’après avoir examiné ce que le compilateur génère. Et, bien sûr, cela devient un processus coûteux, car même le moindre changement que vous apportez autour de l’instruction peut changer la façon dont le compilateur décide de générer le code de sortie.

Maintenant, en ce qui concerne l’instruction switch, j’utiliserais toujours un switch pour rendre le code plus lisible. Le pire qu’un compilateur devrait faire avec une instruction switch équivalente à une instruction if est de générer le même code. Dans plusieurs cas, les instructions de commutateur seront généralement compilées sous forme de table de saut. Mais encore une fois, un compilateur reconnaîtrait if tests comparant une seule variable à un ensemble de valeurs pourraient faire de même. Cependant, je suppose que l’utilisation d’un commutateur permettra au compilateur de reconnaître la situation beaucoup plus facilement.

Si vous souhaitez vraiment tirer le meilleur parti des performances de ce conditionnel, vous pouvez envisager d’utiliser quelque chose comme l’optimisation guidée de profil de MSVC (PGO ou ‘pogo’), qui utilise les résultats des parsings de profilage pour optimiser la manière dont les conditionnelles sont générées. Je ne sais pas si GCC a des capacités similaires.

Je ne suis pas sûr du compilateur C #, mais je sais que dans l’assembly, une instruction switch peut en réalité être programmée comme un saut vers une ligne spécifique, plutôt que d’évaluer l’expression comme une instruction if. Comme dans une sélection, vous avez toutes les constantes, les cas sont traités comme des numéros de ligne et vous passez directement au numéro de ligne (valeur de cas) transmis sans aucune évaluation. Cela fait que l’ordre des déclarations de cas n’a pas vraiment d’importance.

Je présume que vous savez que ce ne sera important que s’il s’agit d’un point chaud. La meilleure façon de savoir s’il s’agit d’un hotspot consiste à exécuter le code, à échantillonner le compteur de programme et à voir s’il est enregistré plus de 10% du temps. S’il s’agit d’un point chaud, voyez combien de temps il faut consacrer à if ou switch . Généralement, il est négligeable, à moins que votre Block 1 et / ou votre Block 2 ne fassent presque rien . Vous pouvez utiliser un profileur pour cela. Je fais juste une pause à plusieurs resockets.

Si vous n’êtes pas familier avec le langage assembleur, je suggérerais de l’apprendre suffisamment pour comprendre ce que le compilateur génère. C’est intéressant et pas difficile.

Comme d’autres l’ont dit, cela dépend de nombreux facteurs, notamment le nombre de cas, comment l’optimisation est effectuée et l’architecture sur laquelle vous travaillez. Pour un aperçu intéressant, voir http://ols.fedoraproject.org/GCC/Reprints-2008/sayle-reprint.pdf

Si vous mettez les cas qui surviennent le plus souvent en premier, cela optimisera légèrement le code et, en raison de la manière dont les instructions de commutateur fonctionnent, il en va de même. Lorsque le programme se met en switch et trouve un cas vrai, il l’exécutera et appuiera sur break, ce qui sortira de la boucle. Votre pensée est correcte.

Cependant, j’estime que cette optimisation est assez minime et que si cela ralentit votre temps de développement, cela n’en vaut probablement pas la peine. De plus, si vous devez modifier votre stream de programme de manière drastique pour y remédier, cela n’en vaut probablement pas la peine. Vous ne économisez que quelques cycles au plus et vous ne verrez probablement jamais d’amélioration.