最近、アマゾンでインタビューを行いました。コーディングセッション中に、インタビュアーは、メソッドで変数を宣言する理由を尋ねました。私は私のプロセスを説明し、彼はより少ない変数で同じ問題を解決するように私に挑戦しました。たとえば(これはインタビューからのものではありませんでした)、私はメソッドAから始めて、その後improvedint s
を削除して、メソッドB、に変更します。彼は満足し、これによりこの方法によるメモリ使用量が削減されると述べました。
私はその背後にある論理を理解していますが、私の質問は:
方法Aと方法B、またはその逆を使用するのはいつ適切ですか?
int s
が宣言されているため、メソッドAのメモリ使用量が増えることがわかりますが、1回の計算しか必要ありません。すなわちa + b
。一方、メソッドBのメモリ使用量は少なくなりますが、2つの計算、つまりa + b
を2回実行する必要があります。あるテクニックを他のテクニックよりもいつ使用しますか?または、いずれかの手法が常に他の手法よりも優先されますか? 2つの方法を評価するときに考慮すべき点は何ですか?
private bool IsSumInRange(int a, int b)
{
int s = a + b;
if (s > 1000 || s < -1000) return false;
else return true;
}
private bool IsSumInRange(int a, int b)
{
if (a + b > 1000 || a + b < -1000) return false;
else return true;
}
何が起きるか、何が起きないかを推測するのではなく、見てみましょうか。 C#コンパイラが手元にないため、C++を使用する必要があります( C#の例を参照してくださいVisualMelon から) 、しかし私は同じ原則が関係なく適用されると確信しています。
インタビューで出会った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
_
スタックアドレスから、mov %edi,-0x4(%rbp)
が使用するスタックアドレス(たとえば、mov %edi,-0x14(%rbp)
の_-0x4
_とIsSumInRangeWithVar()
の_-0x14
_)を比較できます。スタック上に16バイト追加。
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
_
それを見てみますか:各バリアントはidenticalです。コンパイラはかなり賢いことを行うことができます: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()
が他の桁よりも1桁速いことに驚いた。だから私はベンチマークの順序を変えてもう一度走った:
_IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
_
したがって、実際に高速化を実現したのは実装に関するものではなく、ベンチマークを実行する順序です。
正直なところ、なぜこれが起こるのかわからないので、これについてもっと深く掘り下げたいです。しかし、私は要点がなされたと信じています:変数として中間値を宣言するかどうかのようなミクロ最適化はほとんど関係ありません。インタプリタ言語または高度に最適化されたコンパイラを使用する場合、最初の目的は、依然として明確なコードを記述することです。
さらに最適化が必要な場合は、benchmarkを使用します。最良の最適化は細部ではなく、アルゴリズムの全体像から得られることを覚えておいてください。pypyは同じ関数を繰り返し評価する場合、cpythonよりも高速なアルゴリズム(JITコンパイラvs解釈)を使用するため、プログラム。また、考慮すべきコード化アルゴリズムがあります。Bツリーを介した検索は、リンクリストよりも高速です。
ジョブに適切なツールとアルゴリズムを使用していることを確認したら、システムの詳細にdeepする準備をします。経験豊富な開発者にとっても、結果は非常に驚くべきものになる可能性があるため、変更を定量化するためのベンチマークが必要になります。
上記の質問に答えるには:
メソッドのメモリ速度とパフォーマンス速度を最適化するのはいつですか?
確立しなければならないことが2つあります。
最初の質問に答えるには、アプリケーションのパフォーマンス要件を理解する必要があります。パフォーマンス要件がない場合は、いずれかの方法で最適化する理由はありません。パフォーマンス要件は、「十分に良い」という場所に到達するのに役立ちます。
あなたが単独で提供した方法はそれ自体ではパフォーマンスの問題を引き起こしませんが、おそらくループ内で大量のデータを処理するため、問題への取り組み方について少し異なる考えを始める必要があります。
パフォーマンスモニターでアプリケーションの動作を確認します。実行中は、CPU、ディスク、ネットワーク、メモリの使用状況に注意してください。他のすべてが適度に使用されている間は、1つ以上のアイテムが使い果たされます。ただし、完璧なバランスに達しない限り、ほとんど発生しません)。
より深く見る必要がある場合、通常はprofilerを使用します。 メモリープロファイラーとプロセスプロファイラーがあり、それらは異なるものを測定します。プロファイリングの動作はパフォーマンスに大きな影響を与えますが、何が問題なのかを見つけるためにコードを計測しています。
CPUとディスクの使用量がピークに達したとしましょう。最初に、「ホットスポット」または他より頻繁に呼び出されるか、処理のかなり長い割合を占めるコードをチェックします。
ホットスポットが見つからない場合は、メモリを調べ始めます。おそらく、必要以上のオブジェクトを作成していて、ガベージコレクションが時間外で動作している可能性があります。
批判的に考えます。以下の変更リストは、得られる投資収益率の順です。
このような状況では、科学的な方法を適用する必要があります。仮説を立て、変更を加え、テストします。パフォーマンスの目標を達成すれば、完了です。そうでない場合は、リストの次の項目に進みます。
太字で質問に答える:
メソッドAとメソッドB、またはその逆を使用するのが適切なのはいつですか?
正直なところ、これはパフォーマンスやメモリの問題に対処するための最後のステップです。メソッドAとメソッドBの影響は、言語によって実際には異なりますおよびプラットフォーム(場合によっては)。
ほぼすべてのコンパイルされた言語は、中途半端な最適化プログラムを使用して、これらの構造のいずれかで同様のコードを生成します。ただし、これらの仮定は、オプティマイザを持たない独自仕様のおもちゃの言語では必ずしも当てはまりません。
どちらがより良い影響を与えるかは、sum
がスタック変数かヒープ変数かによって異なります。これは言語実装の選択です。 C、C++、およびJavaたとえば、int
のような数値プリミティブは、デフォルトではスタック変数です。スタック変数に割り当てることで、コードがメモリに影響を与えることはありません。完全にインライン化されたコードがあります。
2次元配列を最初にコピーするか最初にコピーするかを決定する必要があるCライブラリ(特に古いもの)に見られるその他の最適化は、プラットフォーム依存の最適化です。ターゲットとするチップセットがメモリアクセスを最適化する方法についての知識が必要です。アーキテクチャには微妙な違いがあります。
結論として、最適化は芸術と科学の組み合わせです。これには、いくつかの批判的思考、および問題へのアプローチ方法にある程度の柔軟性が必要です。小さなことを責める前に、大きなことを探してください。
「これはメモリを減らすだろう」-em、いいえ。これが真実であっても(これはまともなコンパイラーには当てはまりません)、その違いは実際の状況ではほとんど無視できます。
ただし、メソッドA *(メソッドAに少し変更を加えたもの)を使用することをお勧めします。
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)
}
しかし、2つの完全に異なる理由によります。
変数s
に説明的な名前を付けると、コードがわかりやすくなります
コードで同じ合計ロジックを2回使用することを回避できるため、コードのDRYが高くなり、変更によるエラーが発生しにくくなります。
あなたはそれらの両方よりもより良いことができます
return (abs(a + b) > 1000);
ほとんどのプロセッサ(およびコンパイラ)は、1回の操作でabs()を実行できます。合計が少なくなるだけでなく、比較も少なくなるため、一般に計算コストが高くなります。また、分岐を削除します。これは、パイプライン処理が不可能になるため、ほとんどのプロセッサでさらに悪化します。
インタビュアーは、他の回答が述べたように、植物の生命であり、技術面接を行うビジネスはありません。
とはいえ、彼の質問は有効です。そして、最適化のタイミングと方法に対する答えはそれが必要であることを証明し、どの部分がそれを必要とするかを正確に証明するためにプロファイルを作成したときです。クヌース氏は、あまりにも重要なセクションをゴールドプレート化したり、(インタビュアーのように)効果のない変更を加えたりする一方で、本当に必要な場所を見逃してしまうのは簡単であるため、時期尚早な最適化がすべての悪の根源であると有名に言っています。あなたがそれが本当に必要であると強く証明するまで、コードの明快さはより重要な目標です。
編集 FabioTuratiは、これが元の論理とは逆の論理的意味であることを正しく指摘しています(私の間違いです!)。これは、Knuthの引用からのさらなる影響を示しています。それを最適化します。
方法Aと方法Bのどちらを使用するのが適切ですか?
ハードウェアは安価です;プログラマーは高価です 。したがって、この質問で2人が無駄にした時間のコストは、おそらくどちらの答えよりもはるかに悪いです。
とにかく、ほとんどの最近のコンパイラーは、ローカル変数をレジスターに最適化する方法を見つけます(スタックスペースを割り当てるのではなく)。そのため、メソッドは実行可能コードに関しておそらく同じです。このため、ほとんどの開発者は、意図を最も明確に伝えるオプションを選択します( 本当にわかりやすいコードの記述(ROC) を参照)。私の意見では、それは方法Aでしょう。
一方、これが純粋にアカデミックな演習である場合は、方法Cで両方の長所を利用できます。
private bool IsSumInRange(int a, int b)
{
a += b;
return (a >= -1000 && a <= 1000);
}
読みやすくするために最適化します。方法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);
}
たった1つのことを行うが、簡単に推論できる小さな方法。
(これは個人的な好みです。私は否定ではなく肯定的なテストが好きです。元のコードは実際に値が範囲外でないかどうかをテストしています。)
要するに、この質問は現在のコンピューティングにはあまり関係がないと思いますが、歴史的な観点からは、これは興味深い思考の練習です。
あなたのインタビュアーは、おそらく神話男月のファンです。本の中で、フレッド・ブルックスは、プログラマーがツールボックスに主要な関数の2つのバージョン(メモリー最適化バージョンとCPU最適化バージョン)を一般的に必要とするであろうと主張しています。フレッドは、マシンに8キロバイトのRAMしかないIBM System/360オペレーティングシステムの開発をリードした経験に基づいています。このようなマシンでは、関数のローカル変数に必要なメモリが重要になる可能性があります。特に、コンパイラーがローカル変数を効果的に最適化しなかった場合(またはコードがアセンブリ言語で直接記述された場合)は特に重要です。
現在の時代では、メソッド内のローカル変数の有無によって顕著な違いが生じるシステムを見つけるのは難しいでしょう。変数が問題になるためには、メソッドは再帰的であり、深い再帰が予想される必要があります。それでも、変数自体が問題を引き起こす前に、スタックの深さを超えてスタックオーバーフロー例外が発生する可能性があります。問題となる可能性がある唯一の実際のシナリオは、再帰的な方法でスタックに割り当てられた非常に大きな配列の場合です。しかし、ほとんどの開発者が大規模な配列の不要なコピーについて2度考えていると私が思うので、それはまたありそうもありません。
代入後s = a + b;変数aおよびbは使用されなくなりました。したがって、完全に脳の損傷を受けたコンパイラーを使用していない場合は、メモリーは使用されません。とにかくaとbに使用されたメモリが再利用されます。
しかし、この関数を最適化することはまったくナンセンスです。スペースを節約できた場合、関数の実行中は8バイトになる可能性があります(関数が戻ったときに回復されます)。時間を節約できれば、それはナノ秒の単一の数値になります。これを最適化することは、時間の無駄です。
ローカル値型の変数はスタックに割り当てられるか、(このような小さなコードの場合は可能性が高い)プロセッサのレジスタを使用し、RAMを確認することはありません。どちらにせよ、彼らは短命であり、心配する必要はありません。潜在的に大きく、寿命が長いコレクションのデータ要素をバッファリングまたはキューイングする必要がある場合は、メモリの使用を検討し始めます。
そして、それはあなたのアプリケーションであなたが最も気にかけていることに依存します。処理速度は?反応時間?メモリフットプリント?保守性?デザインの一貫性は?すべてあなた次第。
他の答えが言ったように、あなたはあなたが最適化しているものを考える必要があります。
この例では、まともなコンパイラーが両方のメソッドに対して同等のコードを生成するので、この決定はランタイムまたはのメモリーには影響しません!
それがが行うことの影響は、コードの可読性です。 (コードは人間だけが読むためのものであり、コンピュータだけではありません。)2つの例に大きな違いはありません。他のすべてが等しい場合、私は簡潔さを美徳と見なすので、おそらく方法Bを選択します。しかし、他のすべてのものが等しいことはめったになく、より複雑な実世界の場合、それは大きな影響を与える可能性があります。
考慮すべき事柄:
答えの多くが指摘しているように、最新のコンパイラでこの関数を調整しようとしても、違いはありません。オプティマイザはおそらく、最善の解決策を見つけることができます(それを証明するためのアセンブラコードを示した答えに賛成票を投じてください)。インタビューのコードは、比較するように求められたコードとは完全に一致していないと述べたので、おそらく実際の例はもう少し理にかなっています。
しかし、この質問をもう一度見てみましょう。これはインタビューの質問です。それで本当の問題は、あなたが仕事に就きたいと思っていると仮定して、どのようにそれに答えるべきですか?
また、インタビュアーが話していることを知っていて、あなたが知っていることを確認しようとしているだけだとします。
オプティマイザを無視すると、最初の変数はスタックに一時変数を作成する可能性がありますが、2番目の変数は作成しませんが、計算を2回実行します。したがって、最初の方がより多くのメモリを使用しますが、より高速です。
とにかく、計算では結果を保存するために一時変数が必要になる場合があるため(比較できるようにするため)、その変数に名前を付けても付けなくても、違いはありません。
次に、実際にはコードが最適化され、すべての変数がローカルであるため、同等のマシンコードが生成される可能性が最も高いと述べます。ただし、使用しているコンパイラーによって異なります(Javaでローカル変数を「最終」として宣言することで、パフォーマンスを向上させることができるようになったのはそれほど前のことではありません)。
いずれにせよ、スタックは独自のメモリページに存在するため、追加の変数によってスタックがページをオーバーフローさせない限り、実際にはそれ以上メモリを割り当てません。オーバーフローする場合は、まったく新しいページが必要です。
より現実的な例は、キャッシュを使用して多くの計算の結果を保持するかどうかの選択である可能性があることを述べます。これは、CPUとメモリの問題を引き起こします。
これはすべて、あなたが話していることを知っていることを示しています。
代わりに読みやすさに焦点を当てた方が良いと言って、最後にそれを残しておきます。この場合は当てはまりますが、インタビューのコンテキストでは、「パフォーマンスについてはわかりませんが、コードは Janet and John ストーリーのように読み取られます」と解釈される場合があります。
あなたがすべきでないことは、コードの最適化がどのように必要でないかについての通常の当たり障りのないステートメントを捨てる、コードをプロファイリングするまで最適化しないこと(これはあなたが悪いコードを自分で見ることができないことを示すだけです)、ハードウェアのコストはプログラマーより低いです、そして喜んでください、喜んでください、Knuthを引用してはいけません。
コードのパフォーマンスは非常に多くの組織で真の問題であり、多くの組織はそれを理解するプログラマーを必要としています。
特に、Amazonなどの組織では、一部のコードに大きな影響力があります。コードスニペットは、数千台のサーバーまたは数百万台のデバイスに展開され、1年の毎日、数十億回呼び出される場合があります。何千もの同様のスニペットが存在する場合があります。悪いアルゴリズムと良いアルゴリズムの違いは、簡単に1000倍になる可能性があります。数を増やして、これらすべてを倍数にする:それは違いを生みます。システムが容量を使い果たした場合、非実行コードの編成に対する潜在的なコストは非常に大きくなるか、致命的にさえなる可能性があります。
さらに、これらの組織の多くは競争の激しい環境で働いています。したがって、競合他社のソフトウェアが既に所有しているハードウェアで問題なく動作している場合や、ソフトウェアがモバイルハンドセットで実行されており、アップグレードできない場合は、より大きなコンピューターを購入するように顧客に伝えることはできません。一部のアプリケーションは、特にパフォーマンスが重要であり(ゲームやモバイルアプリが頭に浮かびます)、応答性や速度に応じて動作したり停止したりする場合があります。
私は20年以上にわたり、システムに障害が発生したり、パフォーマンスの問題が原因で使用できなくなったりする多くのプロジェクトに携わってきました。これらのシステムの最適化で呼び出されましたが、すべての場合、理解できないプログラマーによって作成された不良コードが原因です彼らが書いていたものの影響。さらに、それは決してコードの一部ではなく、どこにでもあります。振り返ってみると、パフォーマンスについて考え始めるのが遅い方法です。被害は解消されました。
コードのパフォーマンスを理解することは、コードの正確性とコードスタイルを理解することと同じように、優れたスキルです。それは実際にはありません。パフォーマンス障害は、機能障害と同じくらいひどい場合があります。システムが機能しない場合は、機能しません。理由は関係ありません。同様に、使用されないパフォーマンスと機能はどちらも悪いものです。
ですから、インタビュアーがパフォーマンスについてあなたに尋ねた場合、私はできるだけ多くの知識を試し、実証することを勧めます。質問が悪いと思われる場合は、その場合は問題にならないと思う理由を丁寧に指摘してください。 Knuthを引用しないでください。
私たちが話しているのはどのようなコンパイラーですか、そしてどのような「メモリ」ですか?あなたの例では、合理的なオプティマイザを想定しているので、式a+b
は通常、そのような演算を行う前にレジスタ(メモリの形式)に格納する必要があります。
したがって、a+b
を2回検出するダムコンパイラについて話している場合、secondの例ではより多くのレジスタ(メモリ)が割り当てられます。ローカル変数にマップされますが、ここでは非常に愚かなコンパイラについて話しています...すべての変数をすべての場所にスタックする別のタイプの愚かなコンパイラを使用している場合を除き、その場合多分最初のものは2番目のものよりも悲しみを最適化させます*。
私はまだそれをスクラッチしたいと思います。2つ目は、
a+b
に3つのレジスタを割り当て、a
とb
をスピルする可能性があるため、ダムコンパイラでより多くのメモリを使用する可能性があると思います。ほとんどのプリミティブオプティマイザについて話している場合、a+b
をs
にキャプチャすると、レジスタ/スタックのスピルの使用を減らすことができます。
これはすべて、測定や逆アセンブルがないというかなりばかげた方法で非常に投機的であり、最悪のシナリオでさえ、これは「メモリ対パフォーマンス」のケースではありません(私が考えることができる最悪のオプティマイザの間でさえ、私たちは話していませんスタック/レジスタのような一時メモリ以外のものについては)、それは純粋に「パフォーマンス」のケースであり、合理的なオプティマイザの中で2つは同等であり、合理的なオプティマイザを使用していない場合、本質的に微視的な最適化にこだわる理由と特に不在の測定?これは、命令の選択やレジスタの割り当てのようなものです。たとえば、スタックがすべてをこぼすインタプリタを使用しているときに、生産性を維持したいと考えている人がいるとは決して期待しません。
メソッドのメモリ速度とパフォーマンス速度を最適化するのはいつですか?
この質問については、もっと広範に取り組むことができれば、2つが正反対であるとは思えないことがよくあります。特に、アクセスパターンがシーケンシャルであり、CPUキャッシュの速度が与えられている場合、非自明な入力でシーケンシャルに処理されるバイト数を減らすと、(ある時点まで)そのデータを高速で処理できるようになります。もちろん、データがはるかに多く、命令と引き換えにはるかに小さい場合、より少ない命令と引き換えに、より大きな形式で順次処理する方が高速になる可能性があるという分岐点があります。
しかし、多くの開発者は、これらのタイプのケースでのメモリ使用量の削減が、処理に費やされる時間の比例的な削減につながる可能性があることを過小評価する傾向があることを発見しました。いくつかの無駄な試みで大きなLUTに到達するまでにメモリアクセスではなく命令にパフォーマンスコストを変換することは、小さな計算を高速化するために非常に人間的に直感的であり、追加のメモリアクセスでパフォーマンスの低下を見つけるだけです。
巨大な配列を介したシーケンシャルアクセスの場合(例のようにローカルスカラー変数を話さない場合)は、シーケンシャルに耕すメモリが少ないほどパフォーマンスが向上するという規則に従います(特に、結果のコードがそうでない場合よりも単純な場合)私の測定とプロファイラーが別の方法で教えてくれるまで、そして重要なことですが、同じように、ディスク上の小さなバイナリファイルを順番に読み取る方が、大きなファイルよりも高速で耕す方が速いと思います(小さいファイルにいくつかの指示が必要な場合でも) )、その仮定が私の測定に適用されなくなることが示されるまで。
最初に正確さを最適化する必要があります。
Int.MaxValueに近い入力値の場合、関数は失敗します。
int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);
合計が-400にオーバーフローするため、これはtrueを返します。この関数は、a = int.MinValue + 200に対しても機能しません(誤って合計すると「400」になります)。
インタビュアーがチャイムを鳴らさない限り、何を探していたのかはわかりませんが、"overflow is real"です。
面接状況では、問題の範囲を明確にするために質問をします。許可される最大および最小の入力値は何ですか?それらを取得したら、呼び出し元が範囲外の値を送信した場合に例外をスローできます。または(C#の場合)、オーバーフロー時に例外をスローするチェックされた{}セクションを使用できます。はい、それはより多くの作業と複雑ですが、時にはそれが必要なものです。
あなたの質問は、「これをまったく最適化する必要がありますか?」でした。
バージョンAとBは、Aを優先する1つの重要な詳細が異なりますが、最適化とは無関係です。コードを繰り返さないでください。
実際の「最適化」は共通部分式の除去と呼ばれ、ほとんどすべてのコンパイラーがこれを行います。いくつかは、最適化がオフになっている場合でも、この基本的な最適化を行います。そのため、これは本当に最適化ではありません(生成されたコードは、ほとんどの場合、まったく同じです)。
しかし、それが最適化されていない場合最適化されていない場合、なぜそれが望ましいのでしょうか。わかりました、あなたは気にしないコードを繰り返さないでください!
まず第一に、誤って条件節の半分を間違えるリスクはありません。しかし、より重要なのは、このコードを読んでいる誰かがif((((wtf||is||this||longexpression))))
エクスペリエンスの代わりに、あなたが何をしようとしているのか即時を見ることができることです。読者が目にするのはif(one || theother)
です。これは良いことです。まれではありませんが、あなたが3年後に自分のコードを読んで「WTFはこれはどういう意味ですか?」その場合、コードが意図をすぐに伝えてくれると、いつでも役に立ちます。共通の部分式が適切に命名されている場合、それが事実です。
また、将来のいつでも、あなたはそれを決定します。 a+b
をa-b
に変更する必要がある場合、2つではなく1つの場所を変更する必要があります。また、2つ目のものを誤って間違えるというリスクは(再び)ありません。
実際の質問について、何を最適化する必要があるかについては、まず最初にコードを正しいにする必要があります。これは絶対に最も重要なことです。正しくないコードは悪いコードです。さらに、正しくないにも関わらず「正常に動作」するか、少なくともがのように見えても、正常に動作します。その後、コードは読み取り可能になります(コードに不慣れな人も読み取り可能)。
最適化に関しては...故意に反最適化されたコードを書くべきではありません。確かに、始める前に設計について考えるべきではないと言っているわけではありません(適切なものを選択するなど)問題のアルゴリズムであり、最も効率の悪いアルゴリズムではありません)。
しかし、ほとんどのアプリケーションでは、ほとんどの場合、最適化コンパイラーを介して妥当なアルゴリズムを使用して正しい読み取り可能なコードを実行した後に得られるパフォーマンスは問題なく、心配する必要はありません。
そうでない場合、つまり、アプリケーションのパフォーマンスが実際に要件を満たさない場合、それから初めて、そのようなローカルでの実行を心配する必要があります。あなたが試みたものとしての最適化。ただし、最上位のアルゴリズムを再考することが望ましいでしょう。アルゴリズムが優れているために、関数を50,000回ではなく500回呼び出す場合、これはマイクロ最適化で3クロックサイクルを節約するよりも大きな影響があります。ランダムなメモリアクセスで常に数百サイクルの間ストールしない場合、これは余分な安価な計算をいくつか行うよりも大きな影響があります。などなど.
最適化は困難な問題であり(そのことについて本全体を書いて終わりを告げることができます)、特定のスポットを盲目的に最適化することに時間を費やすことは(それがボトルネックであるかどうかさえも知らずに)、通常は時間の浪費です。プロファイリングなしでは、最適化を正しく行うことは非常に困難です。
しかし、経験則として、あなたが盲目的に飛んでいて、単に何かを必要とする/したい場合、または一般的なデフォルト戦略として、私はお勧めします「メモリ」を最適化します。
「メモリ」(特に空間的局所性とアクセスパターン)を最適化すると、通常は利点が得られます。これは、かつてすべてが「ほぼ同じ」だった昔とは異なり、最近はRAMは、原則としてできる最も高価なものの1つです(ディスクからの読み取りを除く)。一方、ALUは安価で毎週速くなります。メモリの帯域幅と待機時間は、それほど速く向上しません。良い局所性と適切なアクセスパターンは、データ量の多いアプリケーションでの不適切なアクセスパターンと比較して、実行時に5倍(極端な例では20倍)の違いを簡単にもたらす可能性があります。キャッシュに優しくすれば、幸せな人になります。
前の段落を全体的に見るために、実行できるさまざまなことにどのようなコストがかかるかを検討してください。 a+b
のようなものを実行すると、1サイクルまたは2サイクルかかります(最適化されていない場合)。ただし、CPUは通常、サイクルごとにいくつかの命令を開始でき、非依存命令をパイプライン処理できるため、より現実的には約半サイクルしかかかりません以下。理想的には、コンパイラーがスケジューリングに長けていて、状況によっては、コストがゼロになる可能性があります。
データ(「メモリ」)の取得には、ラッキーでL1の場合は4〜5サイクルかかり、ラッキーでない場合(L2ヒット)は約15サイクルかかります。データがまったくキャッシュにない場合、数百サイクルかかります。無計画なアクセスパターンがTLBの機能を超えている場合(約50エントリで簡単に実行できます)、さらに数百サイクルを追加します。無計画なアクセスパターンが実際にページ違反の原因である場合、最良の場合は数万サイクル、最悪の場合は数百万サイクルかかります。
今考えてみて、最も緊急に避けたいことは何ですか?