何が起こるかもしれないし、起きないかを推測する代わりに、見てみましょう。便利なC#コンパイラーがないためC ++を使用する必要がありますが(VisualMelonのC#の例を参照)、同じ原則が適用されると確信しています。
インタビューで出会った2つの選択肢を含めます。また、abs
いくつかの回答で提案されているように使用するバージョンも含めます。
#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);
}
最適化なしでコンパイルします: g++ -c -o test.o test.cpp
これにより、何が生成されるかを正確に確認できます。 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
スタック上で16バイトの余分なバイトを使用するスタックアドレス(たとえば、-0x4
in mov %edi,-0x4(%rbp)
と-0x14
in mov %edi,-0x14(%rbp)
)から確認できIsSumInRangeWithVar()
ます。
IsSumInRangeWithoutVar()
スタックに中間値を保存するためのスペースを割り当てないためs
、再計算する必要があり、この実装は2命令長くなります。
おかしい、IsSumInRangeSuperOptimized()
見た目はに似ていますが、IsSumInRangeWithoutVar()
最初は-1000、1000秒と比較されます。
次に、最も基本的な最適化のみを使用してコンパイルしましょうg++ -O1 -c -o test.o test.cpp
。結果:
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
それを見ますか:各バリアントは同一です。コンパイラーは非常に賢いことを行うことができます:符号なしの比較abs(a + b) <= 1000
をa + b + 1000 <= 2000
考慮することと同等setbe
であるため、負の数は非常に大きな正の数になります。lea
命令は、実際には1つの命令で、これらすべての加算を実行し、すべての条件分岐を排除することができます。
あなたの質問に答えるために、ほとんどの場合、最適化すべきことはメモリや速度ではなく、読みやすさです。コードを読むことはそれを書くよりもずっと難しく、「最適化」するためにマングルされたコードを読むことは、明確になるように書かれたコードを読むことよりもずっと難しいです。ほとんどの場合、これらの「最適化」は無視できるか、この場合のようにパフォーマンスへの実際の影響はまったくありません。
フォローアップの質問、このコードがコンパイルではなくインタプリタ言語である場合、何が変わりますか?次に、最適化は重要ですか、それとも同じ結果になりますか?
測定してみましょう!私は例を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)),))
Python 3.5.2で実行すると、次の出力が生成されます。
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
Pythonでの逆アセンブリは、バイトコードの「コンパイラ」が最適化の方法であまり機能しないため、それほど興味深いものではありません。
3つの機能のパフォーマンスはほぼ同じです。IsSumInRangeWithVar()
速度がわずかに向上するため、一緒に行きたくなるかもしれません。にさまざまなパラメータを試したときに追加しますがtimeit
、時々IsSumInRangeSuperOptimized()
最も速く出たので、実装の本質的な利点ではなく、違いの原因となる外部要因である可能性があります。
これが本当にパフォーマンスが重要なコードである場合、インタープリター言語は単に非常に貧弱な選択です。同じプログラムをpypyで実行すると、次のようになります:
IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s
JITコンパイルを使用して多くのインタープリターのオーバーヘッドを排除するpypyを使用するだけで、パフォーマンスが1桁または2桁向上しました。私はIsSumInRangeWithVar()
他のものよりも一桁速いことを見てショックを受けました。そこで、ベンチマークの順序を変更して再度実行しました。
IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
したがって、実装を高速化するのは実際には何でもないようですが、ベンチマークを実行する順序です!
私はこれをもっと深く掘り下げたいと思います。なぜなら、正直に言ってこれがなぜ起こるのかわからないからです。ただし、中間値を変数として宣言するかどうかなどのミクロな最適化はほとんど関係がないと私は指摘していると思います。インタプリタ言語または高度に最適化されたコンパイラを使用する場合、最初の目的は依然として明確なコードを記述することです。
さらに最適化が必要な場合は、ベンチマークを行います。最適な最適化は、細部からではなく、より大きなアルゴリズムの図から得られることに注意してください。プログラム。また、考慮すべきコード化されたアルゴリズムもあります。Bツリーを介した検索は、リンクリストよりも高速です。
ジョブに適切なツールとアルゴリズムを使用していることを確認した後、システムの詳細を深く掘り下げる準備をしてください。その結果は、経験豊富な開発者にとっても非常に驚くべきものです。そのため、変更を定量化するためのベンチマークが必要です。