Quand optimiser la vitesse de la mémoire par rapport à la vitesse d'une méthode?

107

J'ai récemment interviewé à Amazon. Lors d'une session de codage, l'intervieweur a demandé pourquoi j'avais déclaré une variable dans une méthode. J'ai expliqué mon processus et il m'a mis au défi de résoudre le même problème avec moins de variables. Par exemple (ce n’était pas de l’interview), j’ai commencé avec la méthode A puis l’ améliorais à la méthode B en supprimant int s. Il était heureux et a déclaré que cela réduirait l'utilisation de la mémoire par cette méthode.

Je comprends la logique derrière cela, mais ma question est la suivante:

Quand est-il approprié d'utiliser la méthode A par rapport à la méthode B et vice versa?

Vous pouvez voir que la méthode A va utiliser plus de mémoire, car elle int sest déclarée, mais elle ne doit effectuer qu'un seul calcul, à savoir a + b. En revanche, la méthode B utilise moins de mémoire, mais doit effectuer deux calculs, à savoir a + bdeux fois. Quand est-ce que j'utilise une technique plutôt que l'autre? Ou bien l'une des techniques est-elle toujours préférée par rapport à l'autre? Quelles sont les choses à considérer lors de l'évaluation des deux méthodes?

Méthode A:

private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

Méthode B:

private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}
Corey P
la source
229
Je suis prêt à parier qu'un compilateur moderne générera le même assemblage pour ces deux cas.
17 du 26
12
J'ai rétabli la question à son état d'origine, car votre modification a invalidé ma réponse - ne faites pas ça, s'il vous plaît! Si vous posez une question sur la façon d'améliorer votre code, ne changez pas la question en améliorant le code de la manière indiquée. Les réponses n'auront donc plus aucun sens.
Doc Brown
76
Attendez une seconde, ils ont demandé à se débarrasser de int stout en étant parfaitement d'accord avec ces nombres magiques pour les limites supérieure et inférieure?
null
34
Rappelez-vous: profil avant l'optimisation. Avec les compilateurs modernes, les méthodes A et B peuvent être optimisées avec le même code (en utilisant des niveaux d'optimisation plus élevés). De plus, avec les processeurs modernes, ils pourraient avoir des instructions qui ne se limitent pas à une seule opération.
Thomas Matthews
142
Ni; optimiser pour la lisibilité.
Andy

Réponses:

148

Au lieu de spéculer sur ce qui peut ou ne peut pas arriver, regardons, allons-nous? Je vais devoir utiliser le C ++ car je n'ai pas de compilateur C # à portée de main ( voir l'exemple C # de VisualMelon ), mais je suis sûr que les mêmes principes s'appliquent malgré tout.

Nous allons inclure les deux alternatives que vous avez rencontrées dans l'interview. Nous allons également inclure une version qui utilise abscomme suggéré par certaines des réponses.

#include <cstdlib>

bool IsSumInRangeWithVar(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

bool IsSumInRangeWithoutVar(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

bool IsSumInRangeSuperOptimized(int a, int b) {
    return (abs(a + b) < 1000);
}

Maintenant, compilez-le sans aucune optimisation: g++ -c -o test.o test.cpp

Nous pouvons maintenant voir précisément ce que cela génère: objdump -d test.o

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   55                      push   %rbp              # begin a call frame
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)  # save first argument (a) on stack
   7:   89 75 e8                mov    %esi,-0x18(%rbp)  # save b on stack
   a:   8b 55 ec                mov    -0x14(%rbp),%edx  # load a and b into edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax  # load b into eax
  10:   01 d0                   add    %edx,%eax         # add a and b
  12:   89 45 fc                mov    %eax,-0x4(%rbp)   # save result as s on stack
  15:   81 7d fc e8 03 00 00    cmpl   $0x3e8,-0x4(%rbp) # compare s to 1000
  1c:   7f 09                   jg     27                # jump to 27 if it's greater
  1e:   81 7d fc 18 fc ff ff    cmpl   $0xfffffc18,-0x4(%rbp) # compare s to -1000
  25:   7d 07                   jge    2e                # jump to 2e if it's greater or equal
  27:   b8 00 00 00 00          mov    $0x0,%eax         # put 0 (false) in eax, which will be the return value
  2c:   eb 05                   jmp    33 <_Z19IsSumInRangeWithVarii+0x33>
  2e:   b8 01 00 00 00          mov    $0x1,%eax         # put 1 (true) in eax
  33:   5d                      pop    %rbp
  34:   c3                      retq

0000000000000035 <_Z22IsSumInRangeWithoutVarii>:
  35:   55                      push   %rbp
  36:   48 89 e5                mov    %rsp,%rbp
  39:   89 7d fc                mov    %edi,-0x4(%rbp)
  3c:   89 75 f8                mov    %esi,-0x8(%rbp)
  3f:   8b 55 fc                mov    -0x4(%rbp),%edx
  42:   8b 45 f8                mov    -0x8(%rbp),%eax  # same as before
  45:   01 d0                   add    %edx,%eax
  # note: unlike other implementation, result is not saved
  47:   3d e8 03 00 00          cmp    $0x3e8,%eax      # compare to 1000
  4c:   7f 0f                   jg     5d <_Z22IsSumInRangeWithoutVarii+0x28>
  4e:   8b 55 fc                mov    -0x4(%rbp),%edx  # since s wasn't saved, load a and b from the stack again
  51:   8b 45 f8                mov    -0x8(%rbp),%eax
  54:   01 d0                   add    %edx,%eax
  56:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax # compare to -1000
  5b:   7d 07                   jge    64 <_Z22IsSumInRangeWithoutVarii+0x2f>
  5d:   b8 00 00 00 00          mov    $0x0,%eax
  62:   eb 05                   jmp    69 <_Z22IsSumInRangeWithoutVarii+0x34>
  64:   b8 01 00 00 00          mov    $0x1,%eax
  69:   5d                      pop    %rbp
  6a:   c3                      retq

000000000000006b <_Z26IsSumInRangeSuperOptimizedii>:
  6b:   55                      push   %rbp
  6c:   48 89 e5                mov    %rsp,%rbp
  6f:   89 7d fc                mov    %edi,-0x4(%rbp)
  72:   89 75 f8                mov    %esi,-0x8(%rbp)
  75:   8b 55 fc                mov    -0x4(%rbp),%edx
  78:   8b 45 f8                mov    -0x8(%rbp),%eax
  7b:   01 d0                   add    %edx,%eax
  7d:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax
  82:   7c 16                   jl     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  84:   8b 55 fc                mov    -0x4(%rbp),%edx
  87:   8b 45 f8                mov    -0x8(%rbp),%eax
  8a:   01 d0                   add    %edx,%eax
  8c:   3d e8 03 00 00          cmp    $0x3e8,%eax
  91:   7f 07                   jg     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  93:   b8 01 00 00 00          mov    $0x1,%eax
  98:   eb 05                   jmp    9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
  9a:   b8 00 00 00 00          mov    $0x0,%eax
  9f:   5d                      pop    %rbp
  a0:   c3                      retq

On peut voir à partir des adresses de la pile (par exemple, -0x4en mov %edi,-0x4(%rbp)fonction de l' -0x14en mov %edi,-0x14(%rbp)) qui IsSumInRangeWithVar()utilise 16 octets supplémentaires sur la pile.

Parce que IsSumInRangeWithoutVar()n'alloue aucun espace sur la pile pour stocker la valeur intermédiaire, sil doit la recalculer, ce qui entraîne une implémentation de 2 instructions de plus.

C'est drôle, IsSumInRangeSuperOptimized()ça ressemble beaucoup IsSumInRangeWithoutVar(), sauf que ça se compare à -1000 en premier et à 1000 secondes.

Maintenant , nous allons compiler avec seulement les optimisations de base: g++ -O1 -c -o test.o test.cpp. Le résultat:

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
   7:   3d d0 07 00 00          cmp    $0x7d0,%eax
   c:   0f 96 c0                setbe  %al
   f:   c3                      retq

0000000000000010 <_Z22IsSumInRangeWithoutVarii>:
  10:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  17:   3d d0 07 00 00          cmp    $0x7d0,%eax
  1c:   0f 96 c0                setbe  %al
  1f:   c3                      retq

0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>:
  20:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  27:   3d d0 07 00 00          cmp    $0x7d0,%eax
  2c:   0f 96 c0                setbe  %al
  2f:   c3                      retq

Voulez-vous regarder cela: chaque variante est identique . Le compilateur est capable de faire quelque chose d'assez intelligent: abs(a + b) <= 1000équivaut à a + b + 1000 <= 2000envisager setbeune comparaison non signée, ainsi un nombre négatif devient un très grand nombre positif. L' leainstruction peut en réalité effectuer tous ces ajouts en une seule instruction et éliminer toutes les branches conditionnelles.

Pour répondre à votre question, l’optimisation n’exige presque toujours ni la mémoire ni la vitesse, mais la lisibilité . Lire du code est beaucoup plus difficile que de l'écrire, et lire du code qui a été mutilé pour "l'optimiser" est beaucoup plus difficile que de lire du code qui a été écrit pour être clair. Le plus souvent, ces "optimisations" ont un impact négligeable, ou comme dans le cas présent, zéro sur les performances.


Question de suivi, qu'est-ce qui change lorsque ce code est dans un langage interprété au lieu d'être compilé? Ensuite, l'optimisation est-elle importante ou a-t-elle le même résultat?

Mesurons! J'ai transcrit les exemples en Python:

def IsSumInRangeWithVar(a, b):
    s = a + b
    if s > 1000 or s < -1000:
        return False
    else:
        return True

def IsSumInRangeWithoutVar(a, b):
    if a + b > 1000 or a + b < -1000:
        return False
    else:
        return True

def IsSumInRangeSuperOptimized(a, b):
    return abs(a + b) <= 1000

from dis import dis
print('IsSumInRangeWithVar')
dis(IsSumInRangeWithVar)

print('\nIsSumInRangeWithoutVar')
dis(IsSumInRangeWithoutVar)

print('\nIsSumInRangeSuperOptimized')
dis(IsSumInRangeSuperOptimized)

print('\nBenchmarking')
import timeit
print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),))

Exécutée avec Python 3.5.2, cela produit la sortie:

IsSumInRangeWithVar
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (s)

  3          10 LOAD_FAST                2 (s)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               4 (>)
             19 POP_JUMP_IF_TRUE        34
             22 LOAD_FAST                2 (s)
             25 LOAD_CONST               4 (-1000)
             28 COMPARE_OP               0 (<)
             31 POP_JUMP_IF_FALSE       38

  4     >>   34 LOAD_CONST               2 (False)
             37 RETURN_VALUE

  6     >>   38 LOAD_CONST               3 (True)
             41 RETURN_VALUE
             42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

IsSumInRangeWithoutVar
  9           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 LOAD_CONST               1 (1000)
             10 COMPARE_OP               4 (>)
             13 POP_JUMP_IF_TRUE        32
             16 LOAD_FAST                0 (a)
             19 LOAD_FAST                1 (b)
             22 BINARY_ADD
             23 LOAD_CONST               4 (-1000)
             26 COMPARE_OP               0 (<)
             29 POP_JUMP_IF_FALSE       36

 10     >>   32 LOAD_CONST               2 (False)
             35 RETURN_VALUE

 12     >>   36 LOAD_CONST               3 (True)
             39 RETURN_VALUE
             40 LOAD_CONST               0 (None)
             43 RETURN_VALUE

IsSumInRangeSuperOptimized
 15           0 LOAD_GLOBAL              0 (abs)
              3 LOAD_FAST                0 (a)
              6 LOAD_FAST                1 (b)
              9 BINARY_ADD
             10 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               1 (<=)
             19 RETURN_VALUE

Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s

Le désassemblage en Python n'est pas très intéressant, car le "compilateur" bytecode ne fait pas beaucoup d'optimisation.

La performance des trois fonctions est presque identique. Nous pourrions être tentés d'y aller en IsSumInRangeWithVar()raison de son gain de vitesse marginal. Bien que j'ajoute que j'essayais différents paramètres timeit, parfois cela se IsSumInRangeSuperOptimized()présentait le plus rapidement, alors je suppose que cela pourrait être dû à des facteurs externes responsables de la différence plutôt qu'à tout avantage intrinsèque de toute implémentation.

S'il s'agit vraiment d'un code critique en termes de performances, un langage interprété est tout simplement un très mauvais choix. En exécutant le même programme avec pypy, je reçois:

IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s

Le simple fait d'utiliser pypy, qui utilise la compilation JIT pour éliminer une bonne partie des frais généraux de l'interprète, a permis d'améliorer les performances d'un ou deux ordres de grandeur. J'ai été assez choqué de voir IsSumInRangeWithVar()que l'ordre de grandeur est plus rapide que les autres. J'ai donc changé l'ordre des repères et j'ai couru à nouveau:

IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s

Il semble donc que ce n’est pas vraiment quelque chose au sujet de la mise en œuvre qui accélère, mais plutôt l’ordre dans lequel je fais le benchmarking!

J'aimerais approfondir cette question car, honnêtement, je ne sais pas pourquoi cela se produit. Mais je crois que l’argument a été formulé: les micro-optimisations telles que la déclaration d’une valeur intermédiaire en tant que variable ou non sont rarement pertinentes. Avec un langage interprété ou un compilateur hautement optimisé, le premier objectif est toujours d’écrire du code clair.

Si une optimisation supplémentaire est nécessaire, comparez . Rappelez-vous que les meilleures optimisations ne proviennent pas des petits détails mais de la plus grande image algorithmique: pypy sera un ordre de grandeur plus rapide pour une évaluation répétée de la même fonction que cpython car il utilise des algorithmes plus rapides (compilateur JIT vs interprétation) pour évaluer la programme. Et il y a aussi l'algorithme codé à considérer: une recherche dans un arbre B sera plus rapide qu'une liste chaînée.

Une fois que vous assurer utilisez les bons outils et des algorithmes pour le travail, soyez prêt à plonger profondément dans les détails du système. Les résultats peuvent être très surprenants, même pour les développeurs expérimentés. C'est pourquoi vous devez disposer d'un point de repère pour quantifier les changements.

Phil Frost
la source
6
Pour fournir un exemple en C #: SharpLab produit un ASM identique pour les deux méthodes (Desktop CLR v4.7.3130.00 (clr.dll) sur x86)
VisualMelon
2
@VisualMelon est assez amusant le contrôle positif: "return (((a + b)> = -1000) && ((a + b) <= 1000));" donne un résultat différent. : sharplab.io/…
Pieter B
12
La lisibilité peut également rendre un programme plus facile à optimiser. Le compilateur peut facilement réécrire pour utiliser une logique équivalente à celle décrite ci-dessus, à condition qu'il puisse réellement déterminer ce que vous essayez de faire. Si vous utilisez beaucoup de bithacks old-school , faites des va-et-vient entre les ints et les pointeurs, réutilisez le stockage mutable, etc., il sera peut-être beaucoup plus difficile pour le compilateur de prouver qu'une transformation est équivalente, et il ne restera que ce que vous avez écrit. , qui peut être sous-optimal.
Leushenko
1
@Corey voir éditer.
Phil Frost
2
@Corey: cette réponse vous dit exactement ce que j'ai écrit dans ma réponse: il n'y a pas de différence lorsque vous utilisez un compilateur décent, mais vous concentrez sur la lisibilité. Bien sûr, il semble mieux fondé - peut-être me croyez-vous maintenant.
Doc Brown
67

Pour répondre à la question posée:

Quand optimiser la vitesse de la mémoire par rapport à la vitesse d'une méthode?

Il y a deux choses que vous devez établir:

  • Qu'est-ce qui limite votre application?
  • Où puis-je récupérer le maximum de cette ressource?

Afin de répondre à la première question, vous devez connaître les exigences de performance de votre application. S'il n'y a pas d'exigences de performance, il n'y a aucune raison d'optimiser d'une manière ou d'une autre. Les exigences de performance vous aident à vous rendre à la place de "assez bon".

La méthode que vous avez fournie seule ne poserait pas de problème de performances, mais peut-être qu'en boucle et en traitant une grande quantité de données, vous devez commencer à penser un peu différemment à la façon dont vous abordez le problème.

Détecter ce qui limite l'application

Commencez à examiner le comportement de votre application avec un moniteur de performances. Surveillez l'utilisation du processeur, des disques, du réseau et de la mémoire pendant son fonctionnement. Un ou plusieurs éléments seront maximisés tandis que tout le reste est utilisé avec modération - à moins que vous n'atteigniez l'équilibre parfait, mais cela ne se produit presque jamais).

Lorsque vous devez regarder plus en profondeur, vous utiliserez généralement un profileur . Il existe des profileurs de mémoire et des profileurs de processus qui mesurent différentes choses. Le profilage a un impact significatif sur les performances, mais vous instrumentez votre code pour découvrir ce qui ne va pas.

Disons que l'utilisation de votre processeur et de votre disque est à son maximum. Vous devez d’abord vérifier les «points chauds» ou le code appelé plus souvent que les autres ou nécessitant un pourcentage de traitement beaucoup plus long.

Si vous ne trouvez pas de points chauds, vous commencerez alors à examiner la mémoire. Peut-être créez-vous plus d'objets que nécessaire et votre récupération de place ne fonctionne plus.

Récupérer la performance

Pense de façon critique. La liste de modifications suivante indique le retour sur investissement que vous obtiendrez:

  • Architecture: rechercher les points de passage de la communication
  • Algorithme: la façon dont vous traitez les données peut devoir changer
  • Points chauds: réduire le nombre de fois que vous appelez le point chaud peut générer un gros bonus
  • Micro-optimisations: ce n'est pas courant, mais vous devez parfois penser à quelques modifications mineures (comme l'exemple que vous avez fourni), en particulier s'il s'agit d'un point chaud de votre code.

Dans de telles situations, vous devez appliquer la méthode scientifique. Proposez une hypothèse, effectuez les modifications et testez-la. Si vous atteignez vos objectifs de performance, vous avez terminé. Sinon, passez à la prochaine chose dans la liste.


Répondre à la question en gras:

Quand est-il approprié d'utiliser la méthode A par rapport à la méthode B et vice versa?

Honnêtement, c’est la dernière étape pour tenter de résoudre les problèmes de performances ou de mémoire. L'impact de la méthode A par rapport à la méthode B sera très différent selon le langage et la plate - forme (dans certains cas).

À peu près n'importe quel langage compilé avec un optimiseur à mi-parcours décent générera un code similaire avec l'une ou l'autre de ces structures. Cependant, ces hypothèses ne restent pas nécessairement vraies dans les langages propriétaires et les langages de jouets ne disposant pas d'optimiseur.

Ce qui aura un meilleur impact dépendra du sumtype de variable de pile ou de pile. C'est un choix d'implémentation linguistique. En C, C ++ et Java, par exemple, les primitives numériques comme un intsont des variables de pile par défaut. Votre code n'a pas plus d'impact sur la mémoire en affectant une variable de pile que vous n'auriez avec un code entièrement en ligne.

Les autres optimisations que vous pouvez trouver dans les bibliothèques C (en particulier les plus anciennes), où vous pouvez avoir à choisir entre copier un tableau à 2 dimensions en premier ou en premier, est une optimisation dépendante de la plate-forme. Cela nécessite une certaine connaissance de la manière dont le chipset que vous ciblez optimise au mieux l’accès à la mémoire. Il existe des différences subtiles entre les architectures.

En bout de ligne, l'optimisation est une combinaison d'art et de science. Cela nécessite une réflexion critique ainsi qu'une certaine souplesse dans la manière dont vous abordez le problème. Recherchez les grandes choses avant de blâmer les petites choses.

Berin Loritsch
la source
2
Cette réponse se concentre principalement sur ma question et ne me rattrape pas sur mes exemples de codage, à savoir la méthode A et la méthode B.
Corey P
18
Je pense que ceci est la réponse générique à "Comment abordez-vous les goulots d'étranglement des performances", mais vous auriez du mal à identifier l'utilisation relative de la mémoire d'une fonction particulière en fonction du fait qu'elle ait 4 ou 5 variables utilisant cette méthode. Je me demande également quelle est la pertinence de ce niveau d'optimisation lorsque le compilateur (ou l'interpréteur) peut l'optimiser ou non.
Eric
@ Eric, comme je l'ai mentionné, la dernière catégorie d'amélioration de la performance serait votre micro-optimisation. La seule façon de bien deviner si cela aura un impact est de mesurer les performances / la mémoire dans un profileur. Il est rare que ces types d’améliorations rapportent des fruits, mais lorsqu’il est difficile de régler les problèmes de performances liés aux simulateurs, quelques modifications bien placées comme celle-ci peuvent faire la différence entre atteindre votre objectif de synchronisation et ne pas l’atteindre. Je pense pouvoir compter sur une main le nombre de fois que cela a été payant en plus de 20 ans d'utilisation de logiciels, mais ce n'est pas nul.
Berin Loritsch
@BerinLoritsch Encore une fois, en général, je suis d'accord avec vous, mais dans ce cas particulier, je ne le suis pas. J'ai fourni ma propre réponse, mais je n'ai personnellement vu aucun outil qui puisse signaler ou même vous donner le moyen d'identifier les problèmes de performances liés à la taille de la mémoire de pile d'une fonction.
Eric
@DocBrown, j'ai remédié à ça. En ce qui concerne la deuxième question, je suis assez d'accord avec vous.
Berin Loritsch
45

"cela réduirait la mémoire" - em, no. Même si cela était vrai (ce qui n'est pas le cas pour un compilateur décent), la différence serait très probablement négligeable pour toute situation réelle.

Cependant, je recommanderais d'utiliser la méthode A * (méthode A avec une légère modification):

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}

mais pour deux raisons complètement différentes:

  • en donnant à la variable sun nom explicatif, le code devient plus clair

  • cela évite d'avoir deux fois la même logique de sommation dans le code, de sorte que le code devient plus sec, ce qui signifie moins d'erreur susceptible de modifications.

Doc Brown
la source
36
Je voudrais le nettoyer encore plus loin et aller avec "return sum> -1000 && sum <1000;".
17 du 26
36
@Corey tout optimiseur décent utilisera un registre de CPU pour la sumvariable, conduisant ainsi à une utilisation nulle de la mémoire. Et même si ce n'est pas le cas, il ne s'agit que d'un mot de mémoire dans une méthode «feuille». Compte tenu du fait que Java ou C # peut être extrêmement coûteux en mémoire, en raison de leur modèle d'objet et de catalogue global, une intvariable locale n'utilise littéralement aucune mémoire visible. C'est une micro-optimisation inutile.
amon
10
@Corey: si c'est " un peu plus complexe", cela ne deviendra probablement pas "une utilisation de mémoire notable". Peut-être si vous construisez un exemple vraiment plus complexe, mais cela en fait une question différente. Notez également que, du fait que vous ne créez pas de variable spécifique pour une expression, pour des résultats intermédiaires complexes, l'environnement d'exécution peut toujours créer en interne des objets temporaires. Par conséquent, cela dépend entièrement des détails de la langue, de l'environnement, du niveau d'optimisation, etc. tout ce que vous appelez "notable".
Doc Brown
8
En plus des points ci-dessus, je suis sûr que le choix de stocker par C # / Java sumserait un détail de mise en œuvre et je doute que quiconque puisse argumenter de manière convaincante sur le point de savoir si un truc idiot, comme éviter un local int, le mènerait ou non. cette quantité d'utilisation de la mémoire à long terme. La lisibilité de l'OMI est plus importante. La lisibilité peut être subjective, mais FWIW, personnellement, je préférerais que vous ne fassiez jamais le même calcul deux fois, pas pour utiliser le processeur, mais parce que je n'ai qu'à inspecter votre ajout une fois lorsque je cherche un bogue.
HJR
2
... notez également que les langages collectés avec les ordures sont en général imprévisibles, "une mer de mémoire" qui (pour C # de toute façon) ne peut être nettoyée que si nécessaire , je me souviens avoir créé un programme qui allouait des gigaoctets de RAM et qui ne faisait que commencer " nettoyer "après lui-même quand la mémoire est devenue rare. Si le CPG n'a pas besoin de fonctionner, cela peut prendre du temps et économiser votre processeur pour des tâches plus urgentes.
Jrh
35

Vous pouvez faire mieux que les deux avec

return (abs(a + b) > 1000);

La plupart des processeurs (et donc des compilateurs) peuvent exécuter abs () en une seule opération. Vous avez non seulement moins de sommes, mais également moins de comparaisons, qui coûtent généralement plus cher en calcul. Cela supprime également les branchements, ce qui est bien pire pour la plupart des processeurs car cela empêche la création de canaux en pipeline.

L’intervieweur, comme d’autres réponses l’ont dit, parle de la vie de l’usine et n’a pas d’activité à mener une interview technique.

Cela dit, sa question est valide. Et la réponse à apporter lorsque vous optimisez et comment, c’est lorsque vous avez prouvé que c’était nécessaire et que vous l’avez profilée pour prouver exactement quelles pièces en avaient besoin . Knuth a déclaré que l'optimisation prématurée est la racine de tout mal, car il est trop facile d'essayer de mettre en valeur des sections sans importance, ou d'apporter des modifications (comme celle de l'intervieweur) qui n'ont aucun effet, tout en manquant les endroits qui en ont vraiment besoin. Jusqu'à ce que vous ayez une preuve irréfutable, c'est vraiment nécessaire, la clarté du code est la cible la plus importante.

Edit FabioTurati souligne à juste titre que c’est le sens logique opposé à l’original (mon erreur!) Et qu’il illustre un impact supplémentaire de la citation de Knuth dans laquelle nous risquons de casser le code tout en essayant de l’optimiser.

Graham
la source
2
@Corey, je suis à peu près sûr que Graham répond à la requête "il m'a mis au défi de résoudre le même problème avec moins de variables" comme prévu. Si je serais l'interviewer, je pense que la réponse, ne bouge pas a+bdans ifet de le faire deux fois. Vous comprenez que c'est faux "Il était ravi et a dit que cela réduirait l'utilisation de la mémoire par cette méthode" - il était gentil avec vous, cachant sa déception avec cette explication dénuée de sens sur la mémoire. Vous ne devriez pas prendre au sérieux de poser la question ici. Avez-vous trouvé un travail? Je suppose que vous ne l'avez pas fait :-(
Sinatr
1
Vous appliquez 2 transformations en même temps: vous avez transformé les 2 conditions en 1, abs()vous en avez également une return, au lieu d’en avoir une lorsque la condition est vraie ("if branch") et une autre quand il est faux ( "autre branche"). Lorsque vous changez le code de cette manière, faites attention: vous risquez d'écrire par inadvertance une fonction qui renvoie true, alors qu'elle devrait retourner false, et inversement. Ce qui est exactement ce qui s'est passé ici. Je sais que vous vous concentrez sur autre chose et que vous avez fait du bon travail à cet égard. Pourtant, cela aurait pu facilement vous coûter le travail ...
Fabio Turati
2
@FabioTurati Bien vu - merci! Je vais mettre à jour la réponse. Et c’est un bon point à propos de la refactorisation et de l’optimisation, ce qui rend la citation de Knuth encore plus pertinente. Nous devrions prouver que nous avons besoin d'optimisation avant de prendre le risque.
Graham
2
La plupart des processeurs (et donc des compilateurs) peuvent exécuter abs () en une seule opération. Malheureusement pas le cas pour les entiers. ARM64 a une négation conditionnelle qu'il peut utiliser si les drapeaux sont déjà définis à partir d'un adds, et ARM a prédicté reverse-sub ( rsblt= reverse-sub si less-tha), mais tout le reste nécessite plusieurs instructions supplémentaires à implémenter abs(a+b)ou abs(a). godbolt.org/z/Ok_Con affiche les sorties xm , ARM, AArch64, PowerPC, MIPS et RISC-V. Ce n'est qu'en transformant la comparaison en une vérification de la portée (unsigned)(a+b+999) <= 1998Uque gcc peut l'optimiser comme dans la réponse de Phil.
Peter Cordes
2
Le code "amélioré" dans cette réponse est toujours faux, car il produit une réponse différente pour IsSumInRange(INT_MIN, 0). Le code d'origine retourne falseparce que INT_MIN+0 > 1000 || INT_MIN+0 < -1000; mais le code "nouveau et amélioré" revient truecar abs(INT_MIN+0) < 1000. (Ou, dans certaines langues, une exception ou un comportement indéfini sera lancé. Vérifiez vos listes locales.)
Quuxplusone
16

Quand est-il approprié d'utiliser la méthode A par rapport à la méthode B et vice versa?

Le matériel est bon marché; les programmeurs sont chers . Donc, le coût du temps que vous avez gaspillé sur cette question est probablement bien pire que la réponse.

Quoi qu’il en soit, la plupart des compilateurs modernes trouveraient un moyen d’optimiser la variable locale dans un registre (au lieu d’allouer de l’espace pile), de sorte que les méthodes sont probablement identiques en termes de code exécutable. Pour cette raison, la plupart des développeurs choisissent l'option qui communique le plus clairement l'intention (voir Rédaction de code vraiment évident ). À mon avis, ce serait la méthode A.

D'un autre côté, s'il s'agit d'un exercice purement académique, vous pouvez avoir le meilleur des deux mondes avec la méthode C:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}
John Wu
la source
17
a+=bC'est une astuce intéressante, mais je dois mentionner (juste au cas où ce ne serait pas implicite dans le reste de la réponse), de par mon expérience des méthodes qui compliquent le fouillis avec les paramètres peuvent être très difficiles à déboguer et à maintenir.
HJR
1
Je suis d'accord @jrh. Je suis un ardent défenseur du COR, et ce genre de chose est tout sauf.
John Wu
3
"Le matériel est bon marché, les programmeurs sont chers." Dans le monde de l'électronique grand public, cette affirmation est fausse. Si vous vendez des millions d'unités, le fait d'investir 500 000 USD en coûts de développement supplémentaires et d'économiser 0,10 USD sur les coûts de matériel par unité représente un très bon investissement.
Bart van Ingen Schenau
2
@JohnWu: Vous avez simplifié le ifcontrôle, mais vous avez oublié d'annuler le résultat de la comparaison. votre fonction revient maintenant truequand a + bn'est pas dans la plage. Ajoutez un !à l'extérieur de la condition ( return !(a > 1000 || a < -1000)), ou distribuez les !tests return a <= 1000 && a >= -1000;return -1000 <= a && a <= 1000;
inverses
1
@JohnWu: Toujours légèrement en retrait, la logique distribuée nécessite <=/ >=, pas </ >(avec </ >, 1000 et -1000 sont traités comme étant hors plage, le code d'origine les a traités comme s'ils étaient dans la plage).
ShadowRanger
11

J'optimiserais pour la lisibilité. Méthode X:

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}

Petites méthodes qui ne font qu'une chose mais sont faciles à raisonner.

(Ceci est une préférence personnelle, j'aime les tests positifs au lieu de négatifs, votre code d'origine teste réellement si la valeur n'est PAS en dehors de la plage.)

Pieter B
la source
5
Cette. (Les commentaires mentionnés ci-dessus étaient similaires à ceux de la lisibilité). Il y a 30 ans, lorsque nous travaillions avec des machines disposant de moins de 1 Mo de RAM, la compression était indispensable. Tout comme pour le problème du problème Y2K, récupérez quelques centaines de milliers de disques contenant chacun quelques octets de mémoire perdus à cause de vars inutilisés. références, etc. et cela s’ajoute rapidement lorsque vous n’avez que 256 Ko de RAM. Maintenant que nous traitons avec des machines disposant de plusieurs gigaoctets de RAM, économiser même quelques Mo de RAM par rapport à la lisibilité et la maintenabilité du code n'est pas une bonne affaire.
ivanivan
@ivanivan: Je ne pense pas que le "problème du problème de l'an 2000" concerne vraiment la mémoire. Du point de vue de la saisie de données, la saisie de deux chiffres est plus efficace que la saisie de quatre et il est plus facile de conserver les éléments entrés que de les convertir en un autre formulaire.
Supercat
10
Maintenant, vous devez suivre 2 fonctions pour voir ce qui se passe. Vous ne pouvez pas la prendre comme telle, car vous ne pouvez pas dire à partir du nom si ce sont des bornes inclusives ou exclusives. Et si vous ajoutez cette information, le nom de la fonction est plus long que le code pour l'exprimer.
Peter
1
Optimisez la lisibilité et créez des fonctions simples et faciles à mettre en œuvre - d'accord, d'accord. Mais je suis en désaccord fermement que le renommage aet bà number1et la number2lisibilité des aides de quelque façon. De plus, la dénomination des fonctions est incohérente: pourquoi IsSumInRangecoder de manière irréversible la plage si l’ IsValueInRangeaccepter comme argument?
gauche du
La 1ère fonction peut déborder. (Comme le code des autres réponses.) Bien que la complexité du code anti-débordement soit un argument pour l’incorporer à une fonction.
philipxy
6

En bref, je ne pense pas que la question ait beaucoup de pertinence dans l’informatique actuelle, mais d’un point de vue historique, c’est un exercice de réflexion intéressant.

Votre intervieweur est probablement un fan du mois de l'homme mythique. Dans le livre, Fred Brooks explique que les programmeurs auront généralement besoin de deux versions des fonctions principales dans leur boîte à outils: une version optimisée pour la mémoire et une version optimisée pour le processeur. Fred s’appuie sur son expérience pour diriger le développement du système d’exploitation IBM System / 360, sur lequel les machines ne disposent que de 8 kilo-octets de RAM. Dans de telles machines, la mémoire requise pour les variables locales dans les fonctions pourrait être potentiellement importante, surtout si le compilateur ne les optimisait pas efficacement (ou si le code était écrit directement en langage assembleur).

Dans l'ère actuelle, je pense que vous auriez du mal à trouver un système où la présence ou l'absence d'une variable locale dans une méthode ferait une différence notable. Pour qu'une variable ait de l'importance, la méthode doit être récursive avec une récursion profonde attendue. Même dans ce cas, il est probable que la profondeur de la pile soit dépassée, ce qui entraîne des exceptions de dépassement de capacité avant que la variable elle-même ne provoque un problème. Le seul scénario réel où cela peut poser problème concerne les très grands tableaux alloués sur la pile de manière récursive. Mais cela est également peu probable, car je pense que la plupart des développeurs réfléchiraient à deux fois aux copies inutiles de grands tableaux.

Eric
la source
4

Après l'affectation, s = a + b; les variables a et b ne sont plus utilisées. Par conséquent, aucune mémoire n'est utilisée pour s si vous n'utilisez pas un compilateur complètement endommagé au cerveau; la mémoire utilisée de toute façon pour a et b est réutilisée.

Mais optimiser cette fonction est un non-sens. Si vous pouviez économiser de l'espace, il serait peut-être 8 octets pendant l'exécution de la fonction (ce qui est récupéré lorsque la fonction revient), donc absolument inutile. Si vous pouviez gagner du temps, ce serait un nombre unique de nanosecondes. Optimiser cela est une perte de temps totale.

gnasher729
la source
3

Les variables de type de valeur locale sont allouées sur la pile ou (plus probablement pour de tels petits morceaux de code) utilisent des registres dans le processeur et n'arrivent jamais à voir la mémoire RAM. De toute façon ils sont de courte durée et rien à craindre. Vous commencez à envisager l'utilisation de la mémoire lorsque vous devez mettre en mémoire tampon ou mettre en file d'attente des éléments de données dans des collections potentiellement volumineuses et de longue durée.

Ensuite, cela dépend de ce qui compte le plus pour votre application. Vitesse de traitement? Temps de réponse? Empreinte mémoire? Maintenabilité? La cohérence dans la conception? Tout dépend de toi.

Martin Maat
la source
4
Nitpicking: au moins .NET (la langue de la publication n'est pas spécifiée) ne donne aucune garantie quant à l'attribution de variables locales "sur la pile". Voir "la pile est un détail d'implémentation" par Eric Lippert.
HJR
1
@jrh Les variables locales sur pile ou tas peuvent être un détail d'implémentation, mais si quelqu'un voulait vraiment une variable sur la pile, il y a stackallocet maintenant Span<T>. Peut-être utile dans un point chaud, après le profilage. De plus, certains documents autour des structures impliquent que les types de valeur peuvent être sur la pile, alors que les types de référence ne le seront pas . Quoi qu'il en soit, au mieux, vous éviterez peut-être un peu de GC.
Bob
2

Comme d'autres réponses l'ont dit, vous devez penser à ce que vous optimisez.

Dans cet exemple, je soupçonne que tout compilateur correct générera un code équivalent pour les deux méthodes, de sorte que la décision n’aura aucun effet sur le temps d’exécution ou la mémoire!

Ce que cela affecte, c'est la lisibilité du code. (Le code est destiné aux humains, pas seulement aux ordinateurs.) Il n'y a pas trop de différence entre les deux exemples; quand toutes les autres choses sont égales, je considère la brièveté comme une vertu, alors je choisirais probablement la méthode B. Mais toutes les autres choses sont rarement égales et, dans un cas plus complexe, les effets pourraient être très importants.

Choses à considérer:

  • Est-ce que l'expression intermédiaire a des effets secondaires? S'il appelle des fonctions impures ou met à jour des variables, la dupliquer sera évidemment une question de correction, pas seulement de style.
  • Quelle est la complexité de l'expression intermédiaire? S'il effectue de nombreux calculs et / ou appelle des fonctions, le compilateur peut ne pas être en mesure de l'optimiser, ce qui affectera les performances. (Bien que, comme l'a dit Knuth , «nous devrions oublier les petites efficacités, disons environ 97% du temps».)
  • La variable intermédiaire a-t-elle un sens ? Peut-on lui donner un nom qui aide à expliquer ce qui se passe? Un nom court mais informatif pourrait mieux expliquer le code, tandis qu'un nom dénué de sens ne serait qu'un bruit visuel.
  • Combien de temps dure l'expression intermédiaire? Si elle est longue, alors la dupliquer pourrait rendre le code plus long et plus difficile à lire (surtout s'il force un saut de ligne); sinon, la duplication pourrait être plus courte.
des vertiges
la source
1

Comme de nombreuses réponses l’ont souligné, tenter d’ajuster cette fonction avec des compilateurs modernes ne changera rien. Un optimiseur peut probablement trouver la meilleure solution (vote positif pour la réponse indiquant le code de l'assembleur pour le prouver!). Vous avez déclaré que le code de l'entrevue n'était pas exactement le code que l'on vous a demandé de comparer, alors l'exemple réel est peut-être un peu plus logique.

Mais jetons un autre regard sur cette question: c’est une question d’entrevue. Le vrai problème est donc: comment devriez-vous y répondre en supposant que vous voulez essayer d'obtenir le poste?

Supposons également que l'intervieweur sache de quoi il parle et qu'il essaye simplement de voir ce que vous savez.

Je mentionnerais que, en ignorant l'optimiseur, le premier peut créer une variable temporaire sur la pile, tandis que le second ne le ferait pas, mais effectuerait le calcul deux fois. Par conséquent, le premier utilise plus de mémoire mais est plus rapide.

Vous pouvez mentionner de toute façon qu'un calcul peut nécessiter une variable temporaire pour stocker le résultat (afin qu'il puisse être comparé). Ainsi, si vous nommez cette variable ou non, cela ne fera aucune différence.

Je mentionnerais ensuite qu'en réalité, le code serait optimisé et très probablement un code machine équivalent serait généré car toutes les variables sont locales. Cependant, cela dépend du compilateur que vous utilisez (il n'y a pas si longtemps, je pouvais obtenir une amélioration de performance utile en déclarant une variable locale comme "finale" en Java).

Vous pouvez mentionner que, dans tous les cas, la pile vit dans sa propre page de mémoire. Par conséquent, à moins que votre variable supplémentaire ne provoque le débordement de la pile, elle n'allouera en réalité plus de mémoire. S'il déborde, il voudra une toute nouvelle page.

Je mentionnerais qu'un exemple plus réaliste pourrait être le choix d'utiliser ou non un cache pour conserver les résultats de nombreux calculs ou non, ce qui soulèverait la question du processeur par rapport à la mémoire.

Tout cela montre que vous savez de quoi vous parlez.

Je laisserais jusqu'à la fin de dire qu'il vaudrait mieux se concentrer sur la lisibilité. Bien que cela soit vrai dans le cas présent, dans le contexte de l'interview, cela peut être interprété comme "Je ne sais pas à propos de la performance mais mon code se lit comme une histoire de Janet et John ".

Ce que vous ne devriez pas faire, c’est extraire les déclarations fades habituelles sur le fait que l’optimisation du code n’est pas nécessaire, n’optimisez pas tant que vous n’avez pas profilé le code (cela indique simplement que vous ne pouvez pas voir le mauvais code pour vous-même), le matériel coûte moins cher que les programmeurs. et s'il vous plait, s'il vous plait, ne citez pas Knuth "blah blah prématuré ...".

La performance du code est un réel problème dans de très nombreuses organisations et de nombreuses organisations ont besoin de programmeurs qui la comprennent.

En particulier, avec des organisations telles qu'Amazon, certains codes ont un effet de levier considérable. Un extrait de code peut être déployé sur des milliers de serveurs ou des millions de périphériques et peut être appelé des milliards de fois par jour, tous les jours de l'année. Il peut y avoir des milliers d'extraits similaires. La différence entre un mauvais algorithme et un bon peut être mille fois supérieure. Faites les chiffres et les multiples tout cela: cela fait une différence. Le coût potentiel pour l'organisation d'un code non performant peut être très important, voire fatal, si un système est à court de capacité.

De plus, bon nombre de ces organisations travaillent dans un environnement concurrentiel. Vous ne pouvez donc pas simplement dire à vos clients d'acheter un ordinateur plus grand si le logiciel de votre concurrent fonctionne déjà correctement sur le matériel dont ils disposent ou s'il fonctionne sur un téléphone mobile et qu'il ne peut pas être mis à niveau. Certaines applications sont particulièrement critiques en termes de performances (on pense aux jeux et aux applications mobiles) et peuvent vivre ou mourir en fonction de leur réactivité ou de leur vitesse.

J'ai personnellement travaillé pendant plus de deux décennies sur de nombreux projets où les systèmes ont échoué ou étaient inutilisables en raison de problèmes de performances. On m'a appelé pour optimiser ces systèmes et dans tous les cas, cela était dû à un code mal écrit par des programmeurs qui ne comprenaient pas. l'impact de ce qu'ils écrivaient. De plus, ce n'est jamais un morceau de code, il est toujours partout. Quand j'arrive, il est trop tard pour commencer à penser à la performance: le mal est fait.

Comprendre les performances du code est une bonne compétence, au même titre que la compréhension de l’exactitude et du style de code. Cela vient de la pratique. Les défaillances de performances peuvent être aussi graves que les défaillances fonctionnelles. Si le système ne fonctionne pas, cela ne fonctionne pas. Peu importe pourquoi. De même, les performances et les fonctionnalités qui ne sont jamais utilisées sont mauvaises.

Donc, si l'intervieweur vous pose des questions sur les performances, je vous recommanderais d'essayer de démontrer autant de connaissances que possible. Si la question semble mauvaise, expliquez poliment pourquoi vous pensez que cela ne serait pas un problème dans ce cas. Ne cite pas Knuth.

rghome
la source
0

Vous devez d'abord optimiser pour l'exactitude.

Votre fonction échoue pour les valeurs d'entrée proches de Int.MaxValue:

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

Cela retourne true car la somme déborde à -400. La fonction ne fonctionne pas non plus pour a = int.MinValue + 200. (ajoute de manière incorrecte à "400")

Nous ne saurons pas ce que cherchait l'intervieweur à moins qu'il ne vienne nous entendre, mais "le débordement est réel" .

Dans une situation d’entrevue, posez des questions pour clarifier l’ampleur du problème: Quelles sont les valeurs maximales et minimales autorisées? Une fois que vous les avez, vous pouvez lever une exception si l'appelant soumet des valeurs en dehors de la plage. Ou (en C #), vous pouvez utiliser une section vérifiée {}, qui lève une exception en cas de dépassement de capacité. Oui, c'est plus de travail et compliqué, mais parfois c'est ce qu'il faut.

TomEberhard
la source
Les méthodes n'étaient que des exemples. Ils n'ont pas été écrits pour être corrects, mais pour illustrer la question. Merci pour l'entrée cependant!
Corey P
Je pense que la question de l’entrevue concerne la performance, vous devez donc répondre à l’intention de la question. L'intervieweur ne pose pas de question sur le comportement à la limite. Mais intéressant quand même.
rghome
1
@Corey Bons interlocuteurs sous forme de questions pour 1) évaluer la capacité du candidat à l'égard du problème, comme suggéré par rghome, mais aussi 2) comme une ouverture sur des problèmes plus vastes (comme l'exactitude fonctionnelle non tacite) et la profondeur des connaissances associées - ceci est d'autant plus vrai dans les entretiens de carrière ultérieurs - bonne chance.
Chux
0

Votre question aurait dû être: "Dois-je optimiser cela du tout?".

Les versions A et B diffèrent par un détail important qui rend A préférable, mais cela n’est pas lié à l’optimisation: vous ne répétez pas le code.

L '"optimisation" actuelle est appelée élimination commune des sous-expressions, ce que font à peu près tous les compilateurs. Certains font cette optimisation de base même lorsque les optimisations sont désactivées. Donc, ce n'est pas vraiment une optimisation (le code généré sera presque certainement exactement le même dans tous les cas).

Mais si ce n'est pas une optimisation, alors pourquoi est-ce préférable? Bon, vous ne répétez pas le code, on s'en fiche!

Tout d’abord, vous ne courez pas le risque de vous tromper accidentellement sur la moitié de la clause conditionnelle. Mais plus important encore, quelqu'un qui lit ce code peut dire immédiatement ce que vous essayez de faire, au lieu d'une if((((wtf||is||this||longexpression))))expérience. Ce que le lecteur peut voir if(one || theother), c’est une bonne chose. Pas rarement, il se trouve que vous êtes cette autre personne qui lit votre propre code trois ans plus tard et se dit "WTF, ça veut dire quoi?". Dans ce cas, il est toujours utile que votre code communique immédiatement quelle était l'intention. Avec une sous-expression commune nommée correctement, c'est le cas.
En outre, si à un moment quelconque dans l’avenir, vous décidez que, par exemple, vous devez changer a+bpour a-b, vous devez en changer un.emplacement, pas deux. Et il n’ya aucun risque que le second se trompe par accident.

En ce qui concerne votre question, pourquoi devriez-vous optimiser, votre code devrait d’abord être correct . C'est la chose la plus importante. Un code qui n'est pas correct est un code incorrect, même si, malgré tout, il "fonctionne bien", ou du moins il semble que cela fonctionne bien. Après cela, le code devrait être lisible (lisible par quelqu'un qui ne le connaît pas).
En ce qui concerne l’optimisation ... il ne faut certainement pas délibérément écrire du code anti-optimisé, et je ne dis certainement pas que vous ne devriez pas penser au design avant de commencer (comme choisir le bon algorithme pour le problème, pas le moins efficace).

Mais pour la plupart des applications, la plupart du temps, les performances obtenues après l'exécution d'un code lisible et correct à l'aide d'un algorithme raisonnable via un compilateur d'optimisation sont correctes, il n'y a pas de quoi s'inquiéter.

Si tel est le cas contraire , à savoir si la performance de l'application ne fait satisfait pas aux exigences, et alors seulement , vous devriez vous soucier de faire de telles optimisations locales comme celle que vous avez essayé. De préférence, vous reconsidérerez l’algorithme de niveau supérieur. Si vous appelez une fonction 500 fois au lieu de 50 000 fois en raison d'un meilleur algorithme, cela aura un impact plus important que de sauver trois cycles d'horloge sur une micro-optimisation. Si vous ne décrochez pas plusieurs centaines de cycles sur un accès en mémoire aléatoire tout le temps, cela aura un impact plus important que de faire quelques calculs bon marché supplémentaires, etc.

L’optimisation est une question difficile (vous pouvez écrire des livres entiers à ce sujet sans fin) et consacrer du temps à optimiser aveuglément un endroit donné (sans même savoir si c’est le goulot d’étranglement du tout!) Est généralement du temps perdu. Sans profilage, il est très difficile d’optimiser l’optimisation.

Mais en règle générale, lorsque vous volez à l'aveugle et que vous avez juste besoin de / envie de faire quelque chose , ou en tant que stratégie par défaut générale, je suggérerais d'optimiser la "mémoire".
L’optimisation pour la «mémoire» (en particulier la localisation spatiale et les modèles d’accès) offre généralement un avantage, car contrairement à ce qu’il était une fois où tout était «à peu près pareil», l’accès à la RAM est de nos jours l’une des choses les plus coûteuses (à part la lecture sur disque!) que vous pouvez en principe faire. Tandis que ALU, au contraire, est bon marché et va de plus en plus vite chaque semaine. La bande passante mémoire et la latence ne s'améliorent pas aussi rapidement. Une bonne localisation et de bons schémas d'accès peuvent facilement faire une différence de 5x (20x dans les exemples extrêmes, extraites) au moment de l'exécution par rapport aux schémas d'accès incorrect des applications lourdes en données. Soyez gentil avec vos caches et vous serez une personne heureuse.

Pour mettre le paragraphe précédent en perspective, réfléchissez à ce que les différentes choses que vous pouvez faire vous coûtent. Exécuter quelque chose comme a+bprend (sinon optimisé sur) un ou deux cycles, mais le processeur peut généralement démarrer plusieurs instructions par cycle et peut générer des instructions non dépendantes de manière plus réaliste, ce qui ne vous coûte qu'environ un demi-cycle ou moins. Idéalement, si le compilateur est bon en planification, et en fonction de la situation, le coût pourrait être nul.
Récupérer des données ("mémoire") vous coûte soit 4-5 cycles si vous êtes chanceux et c'est en L1, et environ 15 cycles si vous n'êtes pas aussi chanceux (L2 touché). Si les données ne sont pas du tout dans le cache, cela prend plusieurs centaines de cycles. Si votre modèle d'accès aléatoire dépasse les capacités du TLB (facile à faire avec seulement environ 50 entrées), ajoutez quelques centaines de cycles supplémentaires. Si votre modèle d'accès au hasard provoque en réalité une erreur de page, il vous en coûtera quelques dizaines de milliers de cycles dans le meilleur des cas et plusieurs millions dans le pire des cas.
Maintenant, réfléchis-y, quelle est la chose que tu veux éviter le plus d'urgence?

Damon
la source
0

Quand optimiser la vitesse de la mémoire par rapport à la vitesse d'une méthode?

Après avoir obtenu la fonctionnalité correcte en premier . La sélectivité concerne alors les micro-optimisations.


En tant que question d’entrevue concernant les optimisations, le code provoque la discussion habituelle mais manque l’objectif de niveau supérieur suivant: le code est-il fonctionnellement correct?

C ++ et C et d’autres considèrent le intdébordement comme un problème du a + b. Il n’est pas bien défini et C l’appelle comportement indéfini . Il n'est pas spécifié de "boucler" - même s'il s'agit du comportement courant.

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

Une telle fonction appelée IsSumInRange()devrait être bien définie et fonctionner correctement pour toutes les intvaleurs de a,b. Le brut a + bn'est pas. La solution AC pourrait utiliser:

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

Le code ci - dessus pourrait être optimisé en utilisant un large type entier que int, le cas échéant, comme ci - dessous ou la distribution de la sum > N, des sum < -Ntests au sein de la if (a >= 0)logique. Cependant, de telles optimisations ne conduisent pas vraiment à un code émis "plus rapidement" avec un compilateur intelligent, ni à la maintenance supplémentaire d'être intelligent.

  long long sum a;
  sum += b;

Même utiliser abs(sum)est sujet aux problèmes quand sum == INT_MIN.

chux
la source
0

De quel genre de compilateur parle-t-on et de quel type de "mémoire"? Parce que dans votre exemple, en supposant un optimiseur raisonnable, l'expression a+bdoit généralement être stockée dans un registre (une forme de mémoire) avant d'effectuer une telle opération arithmétique.

Donc, si nous parlons d’un compilateur stupide qui rencontre a+bdeux fois, il va allouer plus de registres (mémoire) dans votre deuxième exemple, car votre premier exemple pourrait simplement stocker cette expression une fois dans un seul registre mappé à la variable locale, mais parlons de compilateurs très stupides à ce point ... à moins que vous travaillez avec un autre type de compilateur idiot que pile se répand chaque variable dans tous les sens, dans ce cas , peut - être le premier lui causerait plus de douleur à optimiser que la deuxième*.

Je veux encore gratter cela et pense que le second est susceptible d'utiliser plus de mémoire avec un compilateur muet , même si elle est sujette à empiler les déversements, car il pourrait finir par allouer trois registres pour a+bet le déversement aet bplus. Si nous parlons de l’optimiseur le plus primitif, la capture de a+bsur s«l’aidera» à utiliser moins de registres / de débordements de pile.

Tout cela est extrêmement spéculatif, de manière plutôt stupide, en l'absence de mesures / désassemblage, et même dans le pire des cas, il ne s'agit pas d'un cas "mémoire contre performance" (parce que même parmi les pires optimiseurs auxquels je puisse penser, nous ne parlons pas à propos de tout sauf de la mémoire temporaire comme pile / registre), c’est au mieux un cas de "performance", et parmi tout optimiseur raisonnable, les deux sont équivalents, et si l’on n’utilise pas d’optimiseur raisonnable, pourquoi est-il obsédé par l’optimisation mesures particulièrement absentes? Cela ressemble à la sélection d'instructions / attribution de registre au niveau de l'assemblage, ce que je ne m'attendrais pas à ce que quiconque cherche à rester productif le fasse en utilisant, par exemple, un interprète dont la pile déverse tout.

Quand optimiser la vitesse de la mémoire par rapport à la vitesse d'une méthode?

Quant à cette question, si je peux l’aborder plus largement, souvent, je ne trouve pas les deux diamétralement opposés. Surtout si vos modèles d'accès sont séquentiels, et compte tenu de la vitesse du cache de la CPU, une réduction de la quantité d'octets traités séquentiellement pour des entrées non triviales se traduit (jusqu'à un point) par un traitement plus rapide de ces données. Bien sûr, il existe des points de rupture dans lesquels, si les données sont beaucoup, beaucoup plus petites en échange de plusieurs instructions, il peut être plus rapide de traiter séquentiellement sous une forme plus grande en échange de moins d'instructions.

Mais j'ai constaté que de nombreux développeurs ont tendance à sous-estimer à quel point une réduction de l'utilisation de la mémoire dans ces types de cas peut se traduire par une réduction proportionnelle du temps passé à traiter. Il est très humainement intuitif de traduire les coûts de performance en instructions plutôt qu'en accès mémoire, au point d'atteindre de grandes tables de conversion (LUT), dans une tentative vaine d'accélérer de petits calculs, uniquement pour trouver des performances dégradées avec l'accès mémoire supplémentaire.

Pour les cas d’accès séquentiel à travers un très grand tableau (ne parlant pas de variables scalaires locales comme dans votre exemple), j’applique comme règle que moins de mémoire à parcourir séquentiellement se traduit par de meilleures performances, en particulier lorsque le code résultant est plus simple Jusqu'à ce que mes mesures et mon profileur me disent le contraire, et c'est important, de la même façon, je suppose que lire séquentiellement un fichier binaire plus petit sur disque serait plus rapide à parcourir qu'un fichier plus volumineux (même si le plus petit nécessite davantage d'instructions ), jusqu’à démontrer que cette hypothèse ne s’applique plus dans mes mesures.

Énergie de dragon
la source