C'est une situation que je rencontre fréquemment en tant que programmeur inexpérimenté et que je me demande en particulier pour un projet ambitieux et intensif en vitesse que j'essaie d'optimiser. Pour les principaux langages de type C (C, objC, C ++, Java, C #, etc.) et leurs compilateurs habituels, ces deux fonctions fonctionneront-elles tout aussi efficacement? Y a-t-il une différence dans le code compilé?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Fondamentalement, y a-t-il jamais eu un bonus / une pénalité d'efficacité directe lorsque vous break
êtes en avance ou en return
avance? Comment le stackframe est-il impliqué? Existe-t-il des cas spéciaux optimisés? Y a-t-il des facteurs (comme l'inlining ou la taille de "Do stuff") qui pourraient affecter cela de manière significative?
Je suis toujours partisan d'une meilleure lisibilité sur des optimisations mineures (je vois beaucoup foo1 avec la validation des paramètres), mais cela revient si fréquemment que j'aimerais mettre de côté toute inquiétude une fois pour toutes.
Et je suis conscient des pièges de l'optimisation prématurée ... euh, ce sont des souvenirs douloureux.
EDIT: J'ai accepté une réponse, mais la réponse d'EJP explique assez succinctement pourquoi l'utilisation de a return
est pratiquement négligeable (en assemblage, le return
crée une 'branche' à la fin de la fonction, ce qui est extrêmement rapide. La branche modifie le registre du PC et peut également affecter le cache et le pipeline, ce qui est assez minuscule.) Pour ce cas en particulier, cela ne fait littéralement aucune différence car les if/else
et les return
créent la même branche à la fin de la fonction.
Réponses:
Il n'y a aucune différence:
Cela ne signifie aucune différence dans le code généré, même sans optimisation dans deux compilateurs
la source
something()
sera toujours exécuté. Dans la question initiale, OP aDo stuff
etDo diffferent stuff
selon le drapeau. Je ne suis pas sûr que le code généré sera le même.La réponse courte est, aucune différence. Rendez-vous service et arrêtez de vous en préoccuper. Le compilateur d'optimisation est presque toujours plus intelligent que vous.
Concentrez-vous sur la lisibilité et la maintenabilité.
Si vous voulez voir ce qui se passe, créez-les avec des optimisations et regardez la sortie de l'assembleur.
la source
x = <some number>
que lesif(<would've changed>) x = <some number>
branches non utilisées ne peuvent vraiment faire mal. D'un autre côté, à moins que ce ne soit dans la boucle principale d'une opération extrêmement intensive, je ne m'en soucierais pas non plus.Réponses intéressantes: Bien que je sois d'accord avec tous (jusqu'à présent), il y a des connotations possibles à cette question qui sont jusqu'à présent complètement ignorées.
Si l'exemple simple ci-dessus est étendu avec l'allocation de ressources, puis la vérification des erreurs avec une libération potentielle de ressources, l'image peut changer.
Considérez l' approche naïve que les débutants pourraient adopter:
Ce qui précède représenterait une version extrême du style de retour prématuré. Remarquez comment le code devient très répétitif et non maintenable au fil du temps lorsque sa complexité augmente. De nos jours, les gens peuvent utiliser la gestion des exceptions pour les attraper.
Philip a suggéré, après avoir regardé l'exemple goto ci-dessous, d'utiliser un interrupteur / boîtier sans rupture à l'intérieur du bloc catch ci-dessus. On pourrait basculer (typeof (e)) puis passer à travers les
free_resourcex()
appels, mais ce n'est pas trivial et nécessite une considération de conception . Et rappelez-vous qu'un interrupteur / boîtier sans pause est exactement comme le goto avec les étiquettes en guirlande ci-dessous ...Comme l'a souligné Mark B, en C ++, il est considéré comme un bon style de suivre le principe d' acquisition de ressources est l'initialisation , RAII en bref. L'essentiel du concept est d'utiliser l'instanciation d'objets pour acquérir des ressources. Les ressources sont alors automatiquement libérées dès que les objets sont hors de portée et que leurs destructeurs sont appelés. Pour les ressources interdépendantes, un soin particulier doit être pris pour garantir le bon ordre de désallocation et pour concevoir les types d'objets de telle sorte que les données requises soient disponibles pour tous les destructeurs.
Ou dans les jours pré-exception pourrait faire:
Mais cet exemple trop simplifié présente plusieurs inconvénients: Il ne peut être utilisé que si les ressources allouées ne dépendent pas les unes des autres (par exemple, il ne peut pas être utilisé pour allouer de la mémoire, puis ouvrir un descripteur de fichier, puis lire les données du descripteur dans la mémoire ), et il ne fournit pas de codes d'erreur individuels et distinctifs comme valeurs de retour.
Pour garder le code rapide (!), Compact et facilement lisible et extensible, Linus Torvalds a imposé un style différent pour le code du noyau qui traite des ressources, même en utilisant le tristement célèbre goto d'une manière absolument logique :
L'essentiel de la discussion sur les listes de diffusion du noyau est que la plupart des fonctionnalités du langage qui sont "préférées" par rapport à l'instruction goto sont des gotos implicites, comme les énormes if / else en forme d'arbre, les gestionnaires d'exceptions, les instructions loop / break / continue, etc. Et les goto dans l'exemple ci-dessus sont considérés comme ok, car ils ne sautent que sur une petite distance, ont des étiquettes claires et libèrent le code de tout autre encombrement pour garder une trace des conditions d'erreur. Cette question a également été abordée ici sur stackoverflow .
Cependant, ce qui manque dans le dernier exemple est une bonne façon de renvoyer un code d'erreur. Je pensais ajouter un
result_code++
après chaquefree_resource_x()
appel et renvoyer ce code, mais cela compense certains des gains de vitesse du style de codage ci-dessus. Et il est difficile de renvoyer 0 en cas de succès. Peut-être que je suis juste sans imagination ;-)Donc, oui, je pense qu'il y a une grande différence dans la question du codage des retours prématurés ou non. Mais je pense aussi que cela n'apparaît que dans un code plus compliqué qu'il est plus difficile ou impossible de restructurer et d'optimiser pour le compilateur. Ce qui est généralement le cas une fois que l'allocation des ressources entre en jeu.
la source
catch
contenant uneswitch
instruction sans rupture sur le code d'erreur?Même si ce n'est pas vraiment une réponse, un compilateur de production sera bien meilleur pour l'optimisation que vous. Je préférerais la lisibilité et la maintenabilité à ces types d'optimisations.
la source
Pour être précis à ce sujet, le
return
sera compilé dans une branche jusqu'à la fin de la méthode, où il y aura uneRET
instruction ou quoi que ce soit. Si vous l'omettez, la fin du bloc avant leelse
sera compilée dans une branche jusqu'à la fin duelse
bloc. Vous pouvez donc voir que dans ce cas précis, cela ne fait aucune différence.la source
Si vous voulez vraiment savoir s'il existe une différence dans le code compilé pour votre compilateur et votre système particuliers, vous devrez compiler et examiner l'assembly vous-même.
Cependant, dans le grand schéma des choses, il est presque certain que le compilateur peut optimiser mieux que votre réglage fin, et même s'il ne le peut pas, il est très peu probable qu'il ait réellement une importance pour les performances de votre programme.
Au lieu de cela, écrivez le code de la manière la plus claire pour que les humains le lisent et le maintiennent, et laissez le compilateur faire ce qu'il fait le mieux: générer le meilleur assembly possible à partir de votre source.
la source
Dans votre exemple, le retour est perceptible. Qu'arrive-t-il à la personne qui effectue le débogage lorsque le retour est une page ou deux au-dessus / en dessous où // des choses différentes se produisent? Beaucoup plus difficile à trouver / voir quand il y a plus de code.
la source
Je suis tout à fait d'accord avec blueshift: la lisibilité et la maintenabilité avant tout !. Mais si vous êtes vraiment inquiet (ou que vous voulez simplement savoir ce que fait votre compilateur, ce qui est certainement une bonne idée à long terme), vous devriez vous chercher.
Cela signifiera utiliser un décompilateur ou regarder la sortie du compilateur de bas niveau (par exemple, le langage d'assemblage). En C #, ou tout autre langage .Net, les outils documentés ici vous donneront ce dont vous avez besoin.
Mais comme vous l'avez vous-même observé, il s'agit probablement d'une optimisation prématurée.
la source
From Clean Code: Un manuel de l'artisanat logiciel agile
dans le code, le lecteur devra simplement naviguer vers la fonction et perdre du temps à lire foo (indicateur booléen)
Une base de code mieux structurée vous donnera une meilleure opportunité d'optimiser le code.
la source
Une école de pensée (je ne me souviens pas de l'egghead qui l'a proposé pour le moment) est que toutes les fonctions ne devraient avoir qu'un seul point de retour d'un point de vue structurel pour rendre le code plus facile à lire et à déboguer. Cela, je suppose, est plus pour la programmation du débat religieux.
Une raison technique pour laquelle vous souhaiterez peut-être contrôler quand et comment une fonction se termine et qui enfreint cette règle est lorsque vous codez des applications en temps réel et que vous voulez vous assurer que tous les chemins de contrôle à travers la fonction prennent le même nombre de cycles d'horloge pour se terminer.
la source
Je suis content que vous ayez soulevé cette question. Vous devez toujours utiliser les branches lors d'un retour anticipé. Pourquoi s'arrêter là? Fusionnez toutes vos fonctions en une si vous le pouvez (au moins autant que vous le pouvez). C'est faisable s'il n'y a pas de récursivité. En fin de compte, vous aurez une fonction principale massive, mais c'est ce dont vous avez besoin / que vous voulez pour ce genre de chose. Ensuite, renommez vos identifiants pour qu'ils soient aussi courts que possible. De cette façon, lorsque votre code est exécuté, moins de temps est passé à lire les noms. Ensuite, faites ...
la source