Anstatt darüber zu spekulieren, was passieren kann oder nicht, schauen wir uns das an. Ich muss C ++ verwenden, da ich keinen C # -Compiler zur Hand habe ( siehe auch das C # -Beispiel von VisualMelon ), aber ich bin sicher, dass die gleichen Prinzipien unabhängig davon gelten.
Wir werden die beiden Alternativen, denen Sie begegnet sind, in das Interview aufnehmen. Wir werden auch eine Version hinzufügen, die abs
wie in einigen Antworten vorgeschlagen verwendet wird.
#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);
}
Jetzt kompiliere es ohne jegliche Optimierung: g++ -c -o test.o test.cpp
Jetzt können wir genau sehen, was dies erzeugt: 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
Wir können anhand der Stapeladressen (z. B. das In -0x4
im mov %edi,-0x4(%rbp)
Vergleich zum -0x14
In mov %edi,-0x14(%rbp)
) erkennen, dass IsSumInRangeWithVar()
16 zusätzliche Bytes auf dem Stapel verwendet werden.
Da IsSumInRangeWithoutVar()
der Stack keinen Speicherplatz zum Speichern des Zwischenwerts zuweist s
, muss er neu berechnet werden, was dazu führt, dass diese Implementierung 2 Anweisungen länger dauert.
Komisch, IsSumInRangeSuperOptimized()
sieht sehr ähnlich aus IsSumInRangeWithoutVar()
, außer dass es zuerst -1000 und dann 1000 Sekunden sind.
Lassen Sie uns jetzt kompilieren nur mit den grundlegendsten Optimierungen: g++ -O1 -c -o test.o test.cpp
. Das Ergebnis:
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
Würden Sie sich das ansehen: Jede Variante ist identisch . Der Compiler ist in der Lage , etwas ganz klug zu tun: abs(a + b) <= 1000
entsprechen unter a + b + 1000 <= 2000
Berücksichtigung setbe
ist einen nicht signierten Vergleich, so dass eine negative Zahl eine sehr große positive Zahl wird. Der lea
Befehl kann tatsächlich alle diese Hinzufügungen in einem Befehl ausführen und alle bedingten Verzweigungen beseitigen.
Für die Beantwortung Ihrer Frage müssen Sie fast immer nicht den Speicher oder die Geschwindigkeit optimieren, sondern die Lesbarkeit . Das Lesen von Code ist viel schwieriger als das Schreiben, und das Lesen von Code, der zur "Optimierung" missbraucht wurde, ist viel schwieriger als das Lesen von Code, der klar geschrieben wurde. Meistens sind diese "Optimierungen" vernachlässigbar oder haben in diesem Fall keine tatsächlichen Auswirkungen auf die Leistung.
Folgefrage: Was ändert sich, wenn dieser Code in einer interpretierten Sprache statt kompiliert ist? Ist die Optimierung dann wichtig oder hat sie das gleiche Ergebnis?
Messen wir! Ich habe die Beispiele in Python transkribiert:
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)),))
Führen Sie mit Python 3.5.2 Folgendes aus:
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
Das Zerlegen in Python ist nicht sonderlich interessant, da der Bytecode "Compiler" nicht viel zur Optimierung beiträgt.
Die Leistung der drei Funktionen ist nahezu identisch. Wir könnten in Versuchung IsSumInRangeWithVar()
geraten, dies zu tun, da es nur einen geringen Geschwindigkeitszuwachs gibt. Ich füge zwar hinzu, als ich verschiedene Parameter ausprobierte timeit
, IsSumInRangeSuperOptimized()
kam aber manchmal am schnellsten heraus, sodass ich vermute, dass dies eher externe Faktoren sind, die für den Unterschied verantwortlich sind, als ein wesentlicher Vorteil einer Implementierung.
Wenn dies wirklich leistungskritischer Code ist, ist eine interpretierte Sprache einfach eine sehr schlechte Wahl. Wenn ich das gleiche Programm mit pypy laufen lasse, bekomme ich:
IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s
Allein die Verwendung von pypy, das die JIT-Kompilierung verwendet, um einen Großteil des Interpreter-Overheads zu eliminieren, hat zu einer Leistungsverbesserung von 1 oder 2 Größenordnungen geführt. Ich war ziemlich schockiert zu sehen, dass IsSumInRangeWithVar()
es eine Größenordnung schneller ist als die anderen. Also habe ich die Reihenfolge der Benchmarks geändert und bin noch einmal gelaufen:
IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
Anscheinend geht es also nicht um die schnelle Implementierung, sondern um die Reihenfolge, in der ich das Benchmarking durchführe!
Ich würde gerne näher darauf eingehen, weil ich ehrlich gesagt nicht weiß, warum dies passiert. Aber ich glaube, es wurde darauf hingewiesen: Mikrooptimierungen wie die Angabe eines Zwischenwerts als Variable sind selten relevant. Bei einer interpretierten Sprache oder einem hochoptimierten Compiler besteht das erste Ziel immer noch darin, klaren Code zu schreiben.
Wenn weitere Optimierungen erforderlich sein könnten, Benchmarking . Denken Sie daran, dass die besten Optimierungen nicht von den kleinen Details, sondern vom größeren algorithmischen Bild herrühren: pypy wird für die wiederholte Auswertung derselben Funktion um eine Größenordnung schneller sein als cpython, da es schnellere Algorithmen verwendet (JIT-Compiler vs. Interpretation), um das zu evaluieren Programm. Und der codierte Algorithmus ist ebenfalls zu berücksichtigen: Eine Suche in einem B-Baum ist schneller als eine verknüpfte Liste.
Nachdem Sie sichergestellt haben, dass Sie die richtigen Tools und Algorithmen für den Job verwenden, sollten Sie sich darauf vorbereiten, tief in die Details des Systems einzutauchen. Die Ergebnisse können selbst für erfahrene Entwickler sehr überraschend sein. Aus diesem Grund müssen Sie einen Benchmark haben, um die Änderungen zu quantifizieren.