Mon compilateur ignorera-t-il le code inutile?

Je suis passé par quelques questions sur le réseau à ce sujet mais je n’ai trouvé aucune réponse à ma question, ou c’est pour une autre langue ou ça ne répond pas totalement (le code mort n’est pas un code inutile) alors voici ma question :

Le code (explicite ou non) inutile est-il ignoré par le compilateur?

Par exemple, dans ce code:

double[] TestRunTime = SomeFunctionThatReturnDoubles; // A bit of code skipped int i = 0; for (int j = 0; j < TestRunTime.Length; j++) { } double prevSpec_OilCons = 0; 

la boucle sera-t-elle supprimée?

J’utilise .net4.5 et vs2013


Le fond est que je maintiens beaucoup de code (que je n’ai pas écrit) et je me demandais si le code inutile devrait être une cible ou si je pouvais laisser le compilateur s’en occuper.

Eh bien, vos variables i et prevSpec_OilCons , si elles ne sont utilisées nulle part, seront optimisées, mais pas votre boucle.

Donc, si votre code ressemble à:

 static void Main(ssortingng[] args) { int[] TestRunTime = { 1, 2, 3 }; int i = 0; for (int j = 0; j < TestRunTime.Length; j++) { } double prevSpec_OilCons = 0; Console.WriteLine("Code end"); } 

sous ILSpy ce sera:

 private static void Main(ssortingng[] args) { int[] TestRunTime = new int[] { 1, 2, 3 }; for (int i = 0; i < TestRunTime.Length; i++) { } Console.WriteLine("Code end"); } 

Étant donné que la boucle comporte quelques déclarations, comme la comparaison et l’incrémentation, elle pourrait être utilisée pour mettre en œuvre un délai / période d’attente relativement court. (bien que pas une bonne pratique pour le faire) .

Considérons la boucle suivante, qui est une boucle vide, mais il faudra beaucoup de temps pour s'exécuter.

 for (long j = 0; j < long.MaxValue; j++) { } 

La boucle dans votre code n'est pas un code mort, en ce qui concerne le code mort, le code suivant est un code mort et sera optimisé.

 if (false) { Console.Write("Shouldn't be here"); } 

La boucle ne sera même pas supprimée par le jitter .NET. Basé sur cette réponse

La boucle ne peut être supprimée , le code n’est pas mort , par exemple:

  // Just some function, right? private static Double[] SomeFunctionThatReturnDoubles() { return null; } ... double[] TestRunTime = SomeFunctionThatReturnDoubles(); ... // You'll end up with exception since TestRunTime is null for (int j = 0; j < TestRunTime.Length; j++) { } ... 

Habituellement, le compilateur ne peut tout simplement pas prédire tous les résultats possibles de SomeFunctionThatReturnDoubles et c'est pourquoi il préserve la boucle

Dans votre boucle, il y a deux opérations implicites dans chaque itération. Un incrément:

 j++; 

et comparaison

 j 

Donc, la boucle n'est pas vide bien que cela semble être le cas. Il y a quelque chose qui est exécuté à la fin et cela n'est pas ignoré par le compilateur bien sûr.

Cela se produit également dans d'autres boucles.

Il ne sera pas ignoré. Cependant, quand vous arriverez à IL, il y aura une instruction de saut, donc le for sera exécuté comme s’il s’agissait d’une instruction if. Il exécutera également le code pour ++ et length, comme @Fleve l’a mentionné. Ce sera juste du code supplémentaire. Pour des raisons de lisibilité, ainsi que pour respecter les normes du code, je supprimerais le code si vous ne l’utilisez pas.

Le code (explicite ou non) inutile est-il ignoré par le compilateur?

Vous ne pouvez pas facilement déterminer que c’est inutile, donc le compilateur ne peut pas non plus. Le getter de TestRunTime.Length peut avoir des effets secondaires, par exemple.

Le fond est que je maintiens beaucoup de code (que je n’ai pas écrit) et je me demandais si le code inutile devrait être une cible

Avant de refactoriser un morceau de code, vous devez vérifier ce qu’il fait pour pouvoir le modifier et dire ensuite qu’il a toujours le même résultat. Les tests unitaires sont un excellent moyen de le faire.

Le JIT est fondamentalement capable de supprimer le code mort. Ce n’est pas très complet. Les variables et expressions mortes sont tuées de manière fiable. C’est une optimisation facile sous forme SSA.

Je ne suis pas sûr du stream de contrôle. Si vous imbriquez deux boucles, seule celle interne sera supprimée. Je me souviens.

Si vous voulez savoir avec certitude ce qui est supprimé et ce qui ne regarde pas le code x86 généré. Le compilateur C # effectue très peu d’optimisations. Le JIT en fait quelques-uns.

Les JIT 4.5 32 et 64 bits sont des bases de code différentes et ont un comportement différent. Un nouveau JIT (RyuJIT) est à venir et mes tests sont généralement moins bons, parfois meilleurs.

Évidemment, votre compilateur n’ignorera pas le code inutile, mais l’parsingra attentivement et essaiera ensuite de le supprimer s’il effectue des optimisations.

Dans votre cas, la première chose intéressante est de savoir si la variable j est utilisée après la boucle ou non. L’autre chose intéressante est TestRunTime.Length. Le compilateur l’examinera et vérifiera s’il retourne toujours le même résultat, et si oui, s’il a des effets secondaires, et si oui si l’appel a le même effet secondaire que l’appel répété.

Si TestRunTime.Length n’a pas d’effet secondaire et que j n’est pas utilisé, la boucle est supprimée.

Sinon, si l’appel de TestRunTime.Length a plus d’effets secondaires que de l’appeler une fois ou si des appels répétés renvoient des valeurs différentes, la boucle doit être exécutée.

Sinon, j = max (0, TestRunTime.Length).

Ensuite, le compilateur peut déterminer si l’affectation TestRunTime.Length est nécessaire. Il peut être remplacé par du code qui détermine simplement ce que serait TestRunTime.Length.

Alors, bien sûr, votre compilateur n’essaiera pas d’optimisations fantaisistes, ou les règles du langage pourraient être telles qu’il ne puisse pas déterminer ces choses, et vous êtes bloqué.

Pour la plupart, vous ne devriez pas vous soucier de supprimer de manière proactive du code inutile. Si vous rencontrez des problèmes de performances et que votre profileur dit que du code inutile est en train de manger vos cycles d’horloge, alors vous pouvez l’utiliser en mode nucléaire. Cependant, si le code ne fait vraiment rien et n’a pas d’effets secondaires, cela aura probablement peu d’impact sur le temps d’exécution.

Cela dit, la plupart des compilateurs ne sont pas obligés d’effectuer des optimisations. Par conséquent, s’appuyer sur les optimisations du compilateur n’est pas toujours la solution la plus intelligente. Dans de nombreux cas, même une boucle de spin inutile peut s’exécuter assez rapidement. Un spinlock de base qui boucle un million de fois serait compilé dans quelque chose comme mov eax, 0 \ inc eax \ cmp eax, 1000000 \ jnz -8 . Même si nous ne tenions pas compte des optimisations sur processeur, cela ne représente que 3 cycles par boucle (sur une puce de type RISC récente) car il n’y a pas d’access à la mémoire, il n’y aura donc pas d’invalidation du cache. Sur un processeur à 1 GHz, cela ne représente que 3 000 000/1 000 000 000 de secondes, soit 3 millisecondes. Ce serait un succès considérable si vous tentiez de l’exécuter 60 fois par seconde, mais dans de nombreux cas, cela ne sera probablement même pas perceptible.

Une boucle comme celle que j’ai décrite serait presque définitivement optimisée pour mov eax 1000000 , même dans un environnement JIT. Il serait probablement optimisé plus que cela, mais sans autre contexte, cette optimisation est raisonnable et ne causerait aucun effet néfaste.

tl; dr: Si votre profileur dit qu’un code mort / inutile utilise une quantité notable de vos ressources d’exécution, supprimez-le. Ne partez pas en chasse aux sorcières pour le code mort; laissez cela pour le refactor / refactor massif sur la ligne.

Bonus: Si le générateur de code savait que eax ne serait pas lu pour autre chose que la condition de la boucle et souhaitait conserver le spinlock, il pourrait générer mov eax, 1000000 \ dec eax \ jnz -3 et réduire la pénalité du boucle par cycle. La plupart des compilateurs le supprimeraient cependant complètement.

J’ai fait un petit formulaire pour le tester en fonction de quelques idées sur l’utilisation de long.MaxValue , voici mon code de référence:

 public Form1() { InitializeComponent(); Stopwatch test = new Stopwatch(); test.Start(); myTextBox.Text = test.Elapsed.ToSsortingng(); } 

et voici le code avec un code un peu inutile:

 public Form1() { InitializeComponent(); Stopwatch test = new Stopwatch(); test.Start(); for (int i = 0; i < int.MaxValue; i++) { } myTextBox.Text = test.Elapsed.ToString(); } 

Vous remarquerez que j'ai utilisé int.MaxValue au lieu de long.MaxValue , je ne voulais pas passer la journée sur celui-ci.

Comme vous pouvez le voir:

 --------------------------------------------------------------------- | | Debug | Release | --------------------------------------------------------------------- |Ref | 00:00:00.0000019 | 00:00:00.0000019 | |Useless code | 00:00:05.3837568 | 00:00:05.2728447 | --------------------------------------------------------------------- 

Le code n'est pas optimisé. Attendez un peu, je vais essayer avec certains int[] pour tester int[].Lenght

 public Form1() { InitializeComponent(); int[] myTab = functionThatReturnInts(1); Stopwatch test = new Stopwatch(); test.Start(); for (int i = 0; i < myTab.Length; i++) { } myTextBox.Text = test.Elapsed.ToString(); } public int[] functionThatReturnInts(int desiredSize) { return Enumerable.Repeat(42, desiredSize).ToArray(); } 

Et voici les résultats:

 --------------------------------------------- | Size | Release | --------------------------------------------- | 1 | 00:00:00.0000015 | | 100 | 00:00:00 | | 10 000 | 00:00:00.0000035 | | 1 000 000 | 00:00:00.0003236 | | 100 000 000 | 00:00:00.0312673 | --------------------------------------------- 

Donc, même avec des tableaux, il n'est pas optimisé du tout.