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 abs
comme 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, -0x4
en mov %edi,-0x4(%rbp)
fonction de l' -0x14
en 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, s
il 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 <= 2000
envisager setbe
une comparaison non signée, ainsi un nombre négatif devient un très grand nombre positif. L' lea
instruction 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.