先日、アマゾンの面接を受けました。コーディングのセッションで、面接官が「なぜメソッド内で変数を宣言するのか」と質問してきました。私は自分のプロセスを説明し、彼はより少ない変数で同じ問題を解決するように私に挑戦しました。例えば、(これは面接での話ではないのですが)私はメソッドAから始めて、int s
を削除してメソッドBに改善しました。彼は喜んで、この方法によってメモリ使用量を減らすことができると言いました。
その理屈は理解できるのですが、質問です。
**どのような場合に方法Aと方法Bを使い分けるのが適切なのでしょうか、またその逆も同様でしょうか?
メソッド A は int s
が宣言されているため、メモリ使用量が多くなることがわかりますが、計算するのは a + b
のように 1 回だけです。一方、メソッド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;
}
述べられた質問に答えること。
あるメソッドにおいて、メモリとパフォーマンスの速度を最適化するタイミングは?
という質問に答えるために、以下の2つのことを確認する必要があります。
最初の質問に答えるには、アプリケーションのパフォーマンス要件が何であるかを知る必要があります。 もし、性能要件がなければ、どちらかに最適化する理由はありません。 性能要件は、「これで十分だ」というところに到達するのに役立ちます。
しかし、ループの中で大量のデータを処理する場合は、問題への取り組み方を少し変えなければなりません。
パフォーマンスモニターを使って、アプリケーションの挙動を観察し始めます。 実行中のCPU、ディスク、ネットワーク、メモリ使用量に目を向けてください。 1つまたは複数の項目が最大になり、他のすべてが適度に使用されます--完璧なバランスを達成しない限り、それはほとんど起こりません)。
より深く調べる必要がある場合、通常は「プロファイラ」を使用します。 メモリプロファイラ」と「プロセスプロファイラ」があり、それぞれ測定対象が異なります。 プロファイリングはパフォーマンスに大きな影響を与えますが、何が問題なのかを見つけるためにコードを計測しているのです。
例えば、CPUとディスクの使用量がピークに達しているとします。 まず、他のコードよりも頻繁に呼び出されるコードや、処理にかかる時間が非常に長いコードをチェックします。
ホットスポットが見つからない場合は、次にメモリに注目します。 おそらく、必要以上にオブジェクトを生成して、ガベージコレクションが残業しているのでしょう。
批判的に考えましょう。 以下の変更点のリストは、投資対効果の高い順に並んでいます。
このような状況では、科学的な方法を適用する必要があります。 仮説を立て、変更を加え、それをテストする。 パフォーマンス目標が達成できれば、それで完了です。 そうでない場合は、リストの次の項目に進みます。
太字の質問に答える。
A方式とB方式、またその逆はどのような場合に使うのが適切ですか?
正直なところ、これは、パフォーマンスやメモリの問題に対処しようとする際の最後のステップです。 方法Aと方法Bの影響は、言語とプラットフォーム(場合によっては)により本当に異なります。
中途半端なオプティマイザを搭載したコンパイル言語であれば、これらの構造のどちらでも同じようなコードを生成することができます。 しかし、オプティマイザを持たないプロプライエタリな言語やおもちゃの言語では、これらの仮定は必ずしも当てはまらない。
どちらが良い影響を与えるかは、sum
がスタック変数かヒープ変数のどちらであるかによります。 これは言語の実装の選択です。 例えば、C、C++、Java では、int
のような数プリミティブはデフォルトでスタック変数になります。 スタック変数に代入しても、完全にインライン化されたコードと同じように、メモリに影響を与えることはありません。
C のライブラリ(特に古いライブラリ)で見られるような、2 次元配列を下にコピーするか、横にコピーするかを決めるような最適化は、プラットフォームに依存する最適化です。 これは、対象となるチップセットがどのようにメモリ・アクセスを最適化するかについての知識が必要です。 アーキテクチャによって微妙な違いがあるのです。
要するに、最適化とは芸術と科学の融合なのです。 最適化には、批判的な思考と、問題への取り組み方における柔軟性が必要です。 小さなことを責める前に、大きなことを探しましょう。
これはメモリを削減することになります。仮にそうであったとしても(まともなコンパイラではそうではありません)、その差は現実の状況下ではおそらく無視できる程度でしょう。
しかし、私は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
に説明のための名前を付けることで、コードがより明確になります。
変数 s
に説明のための名前をつけることで、コードが明確になり、同じ計算ロジックが2回出てくるのを避けられるので、コードがよりDRYになり、変更時のエラーが少なくなります。