メソッドのメモリとパフォーマンスの速度を最適化するタイミング


107

私は最近Amazonでインタビューしました。コーディングセッション中に、インタビュアーはメソッドで変数を宣言した理由を尋ねました。私は自分のプロセスを説明し、彼はより少ない変数で同じ問題を解決するように私に挑戦しました。例えば、私が開始(これはインタビューからではなかった)方法A、次いで向上にそれを、方法B除去することによってint s。彼は喜んでおり、これはこの方法によってメモリ使用量を減らすだろうと言った。

私はその背後にあるロジックを理解していますが、私の質問は次のとおりです。

方法Aと方法B、またはその逆を使用するのが適切な場合

メソッドAint s宣言されているため、メモリ使用量が高くなることがわかりますが、実行する必要があるのは1つの計算だけa + bです。一方、方法Bではメモリ使用量は少なくなりますが、a + b2回、つまり2回計算する必要があります。あるテクニックを他のテクニックよりもいつ使用するのですか?または、テクニックの1つは常に他よりも優先されますか?2つの方法を評価する際に考慮すべきことは何ですか?

方法A:

private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

方法B:

private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

229
最新のコンパイラーがこれらの両方のケースで同じアセンブリーを生成することは間違いありません。

12
あなたの編集が私の答えを無効にしたので、私は元の状態に質問をロールバックしました-それをしないでください!コードの改善方法について質問する場合は、示された方法でコードを改善して質問を変更しないでください。これにより、回答が無意味に見えます。
Doc Brown

76
ちょっと待って、彼らはint s上界と下界の魔法の数字で完全に元気なうちに取り除くように頼んだ?
nullの

34
注意:最適化する前にプロファイルを作成してください。最新のコンパイラでは、メソッドAとメソッドBを同じコードに最適化できます(より高い最適化レベルを使用)。また、最新のプロセッサでは、1回の操作で追加以上のことを実行する命令を使用できます。
トーマスマシューズ

142
どちらでもない; 可読性を最適化します。
アンディ

回答:


148

何が起こるかもしれないし、起きないかを推測する代わりに、見てみましょう。便利な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バイトの余分なバイトを使用するスタックアドレス(たとえば、-0x4in mov %edi,-0x4(%rbp)-0x14in 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) <= 1000a + 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ツリーを介した検索は、リンクリストよりも高速です。

ジョブに適切なツールとアルゴリズムを使用していることを確認した後、システムの詳細を深く掘り下げる準備をしてください。その結果は、経験豊富な開発者にとっても非常に驚くべきものです。そのため、変更を定量化するためのベンチマークが必要です。


6
C#で例を提供するには:SharpLabは両方のメソッドで同じasmを生成します(x86のDesktop CLR v4.7.3130.00(clr.dll))
VisualMelon

2
@VisualMelonは十分に肯定的なチェックを十分に楽しんでいます: "return(((a + b)> = -1000)&&((a + b)<= 1000));"は異なる結果を与えます。:sharplab.io/...
ピーターB

12
読みやすさにより、プログラムの最適化も容易になる可能性があります。コンパイラは、あなたが何をしようとしているかを実際に把握できる場合にのみ、上記のような同等のロジックを使用するように簡単に書き換えることができます。昔ながらのビットハックを多く使用し、intとポインタの間を行き来し、可変ストレージなどを再利用する場合、コンパイラが変換が同等であることを証明するのがはるかに難しくなる可能性があります。 、これは最適ではない可能性があります。
ルーシェンコ

1
@Corey see edit。
フィルフロスト

2
@Corey:この答えは、実際に私が答えに書いたものを正確に伝えています。まともなコンパイラを使用しても違いはなく、代わりに準備に焦点を当てています。もちろん、それは設立されたように見えます-多分あなたは今私を信じています。
ドックブラウン

67

記載されている質問に答えるには:

メソッドのメモリとパフォーマンスの速度を最適化するタイミング

確立する必要がある2つのことがあります。

  • アプリケーションの制限は何ですか?
  • そのリソースのほとんどをどこで回収できますか?

最初の質問に答えるには、アプリケーションのパフォーマンス要件を知る必要があります。パフォーマンス要件がない場合は、何らかの方法で最適化する理由はありません。パフォーマンス要件は、「十分な」場所に到達するのに役立ちます。

独自に提供したメソッドは、それ自体でパフォーマンスの問題を引き起こすことはありませんが、おそらくループ内で大量のデータを処理する場合、問題へのアプローチ方法について少し違った考え方を始める必要があります。

アプリケーションを制限しているものを検出する

パフォーマンスモニターでアプリケーションの動作を確認します。実行中のCPU、ディスク、ネットワーク、およびメモリ使用量に注意してください。他のすべてが適度に使用されている間、1つまたは複数のアイテムが最大限に使用されます-完全なバランスに達しない限り、それはほとんど起こりません)。

より深く見る必要がある場合、通常はプロファイラーを使用します。あり、メモリプロファイラプロセスのプロファイラは、彼らは別のものを測定します。プロファイリングの行為はパフォーマンスに大きな影響を与えますが、あなたはコードをインスツルメントして、何が悪いのかを見つけています。

CPUとディスクの使用率がピークに達したとしましょう。最初に、「ホットスポット」または残りよりも頻繁に呼び出されるコード、または処理のかなりの割合を占めるコードをチェックします。

ホットスポットが見つからない場合は、メモリを見始めます。おそらく、必要以上のオブジェクトを作成していて、ガベージコレクションが残業している可能性があります。

パフォーマンスの回収

批判的に考えてください。以下の変更点のリストは、得られる投資収益率の順です。

  • アーキテクチャ:通信のチョークポイントを探す
  • アルゴリズム:データの処理方法を変更する必要がある場合があります
  • ホットスポット:ホットスポットを呼び出す頻度を最小限にすると、大きなボーナスが得られます
  • マイクロ最適化:それは一般的ではありませんが、特にコードのホットスポットである場合は、(提供した例のような)微調整を考える必要がある場合があります。

このような状況では、科学的方法を適用する必要があります。仮説を立て、変更を加えてテストします。パフォーマンスの目標を達成したら、完了です。そうでない場合は、リストの次の項目に進みます。


太字の質問に答える:

方法Aと方法B、またはその逆を使用するのが適切な場合

正直なところ、これはパフォーマンスやメモリの問題に対処する最後のステップです。メソッドAとメソッドBの影響は、言語プラットフォームによって(場合によっては)実際に異なります。

まともなオプティマイザを備えたほぼすべてのコンパイル済み言語は、これらの構造のいずれかで同様のコードを生成します。ただし、オプティマイザーを持たないプロプライエタリおよび玩具の言語では、これらの仮定が必ずしも当てはまるわけではありません。

どちらがより良い影響を与えるかsumは、スタック変数かヒープ変数かによって異なります。これは言語実装の選択です。たとえば、C、C ++、およびJavaでは、などの数値プリミティブintはデフォルトでスタック変数です。完全にインライン化されたコードよりも、スタック変数に割り当てることによるコードのメモリへの影響はありません。

2次元配列を最初にコピーするか最初にコピーするかを決定する必要があるCライブラリ(特に古いライブラリ)で見つかる可能性のある他の最適化は、プラットフォームに依存する最適化です。ターゲットとするチップセットがメモリアクセスを最適化する方法についてある程度の知識が必要です。アーキテクチャには微妙な違いがあります。

結論として、最適化は芸術と科学の組み合わせです。いくつかの批判的思考と、問題へのアプローチ方法の柔軟性が必要です。小さなものを責める前に大きなものを探してください。


2
この答えは、ほとんど私の質問に焦点を当て、すなわち私のコーディング例、方法Aおよび方法Bに巻き込まれません
コーリーP

18
これは「パフォーマンスのボトルネックにどのように対処するか」に対する一般的な答えだと思いますが、このメソッドを使用して変数が4つまたは5つあったかどうかに基づいて特定の関数の相対的なメモリ使用量を特定するのは難しいでしょう また、コンパイラー(またはインタープリター)がこれを最適化する場合としない場合とで、このレベルの最適化がどの程度関連するかについても質問します。
エリック

@Eric、私が言ったように、パフォーマンス改善の最後のカテゴリーはあなたのマイクロ最適化です。影響があるかどうかを推測する唯一の方法は、プロファイラーでパフォーマンス/メモリを測定することです。これらのタイプの改善が見返りをもたらすことはまれですが、シミュレーターにあるタイミングに敏感なパフォーマンスの問題では、そのようないくつかの適切に配置された変更が、タイミングターゲットに到達するかどうかの違いになります。私は、ソフトウェアに取り組んで20年以上で成果を上げた回数を数えることができると思いますが、それはゼロではありません。
ベリンロリチュ

@BerinLoritsch繰り返しますが、一般的に私はあなたに同意しますが、この特定のケースではそうではありません。私は独自の答えを提供しましたが、関数のスタックメモリサイズに関連するパフォーマンスの問題を特定したり、潜在的に特定する方法を与えるツールを個人的に見たことはありません。
エリック

@DocBrown、私はそれを改善しました。2番目の質問に関して、私はあなたにほぼ同意します。
ベリンロリチュ

45

「これはメモリを削減します」-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が大きくなり、変更が発生しやすいエラーが少なくなります。


36
さらにクリーンアップして、「return sum> -1000 && sum <1000;」と入力します。

36
@Coreyまともなオプティマイザはsum変数にCPUレジスタを使用するため、メモリ使用量はゼロになります。そして、そうでなくても、これは「リーフ」メソッドのメモリの単一ワードにすぎません。GCやオブジェクトモデルが原因で、非常にメモリを浪費するJavaまたはC#が他の方法でどのように処理されるかを考慮すると、ローカルint変数は文字通り、顕著なメモリを使用しません。これは無意味なマイクロ最適化です。
アモン

10
@Corey:「もう少し複雑」であれば、おそらく「顕著なメモリ使用量」にはなりません。たぶんもっと複雑な例を構築したとしても、それは別の質問になります。式に特定の変数を作成しないという理由だけでなく、複雑な中間結果のために、ランタイム環境は内部的に一時オブジェクトを作成する可能性があるため、言語、環境、最適化レベル、およびあなたが「注目すべき」と呼ぶものは何でも。
Doc Brown

8
上記の点に加えて、C#/ Javaがどのように格納sumするかを選択することは実装の詳細になると確信intしています。長期的なメモリ使用量。IMOの可読性はより重要です。読みやすさは主観的な場合がありますが、FWIW、個人的には、CPU使用率ではなく、同じ計算を2回行わないことをお勧めします。
-jrh

2
...また、一般的にガベージコレクションされた言語は予測不可能な「メモリの大量消費」であり、(とにかく)必要な場合にのみクリーンアップされる可能性があることに注意してください、ギガバイトのRAMを割り当てたプログラムを作成したことを覚えていますメモリーが不足したときに、自動的にクリーンアップします。GCを実行する必要がない場合は、甘い時間を要し、CPUを節約して緊急の問題に対処することができます。
jrh

35

あなたはそれらの両方よりもうまくやることができます

return (abs(a + b) > 1000);

ほとんどのプロセッサー(したがってコンパイラー)は、1回の操作でabs()を実行できます。合計が少なくなるだけでなく、比較も少なくなります。これは通常、計算コストが高くなります。また、分岐も削除します。これは、パイプライン化が不可能になるため、ほとんどのプロセッサでさらに悪化します。

インタビュアーは、他の回答が述べているように、植物の生活であり、技術面接を行うビジネスはありません。

とはいえ、彼の質問は有効です。そして、最適化のタイミングと方法に対する答えは、それが必要であることを証明し、それをプロファイリングして、どの部品がそれを必要とするかを正確に証明することです。Knuthは、重要ではない部分をゴールドプレートにしたり、実際に必要な場所を逃しながら効果のない(インタビュアーのような)変更を行うのは簡単すぎるため、早すぎる最適化がすべての悪の根源であると有名に言っています。確固たる証拠が得られるまで、それは本当に必要です。コードの明快さがより重要なターゲットです。

編集 FabioTuratiは、これがオリジナルとは反対の論理的意味であると正しく指摘しています(私の間違いです!)。これは、最適化を試みている間にコードを壊すリスクがあるというKnuthの引用からのさらなる影響を示しています。


2
@ Corey、Grahamがリクエストを「より少ない変数で同じ問題を解決するように私に挑戦した」と予想どおりに確信していると確信しています。私は面接だろうならば、私は動いていない、という答えを期待するa+bif二回それをやって。あなたはそれが間違って理解して、「彼は喜んでいたし、これは、この方法により、メモリ使用量を削減すると述べ、」彼は、メモリについては、この無意味な説明を彼の失望を隠し、あなたに素敵でした- 。ここで質問することを真剣に考えるべきではありません。仕事に就きましたか?私はあなたがしなかったと思う:-(
Sinatr

1
同時に2つの変換を適用しています:を使用して2つの条件を1に変更しabs()return条件がtrue( "if branch")の場合とfalse(falseの場合) 「その他のブランチ」)。このようなコードを変更するときは注意が必要です。falseを返す必要があるときにtrueを返す関数を誤って作成したり、その逆を行う危険があります。これがまさにここで起こったことです。あなたは別のことに集中していることを知っています、そしてあなたはそれで素晴らしい仕事をしました。それでも、これは簡単に...あなたの仕事を要するかもしれない
ファビオ・地下鉄Turati

2
@FabioTuratiよく見つけた-ありがとう!答えを更新します。また、リファクタリングと最適化についての良い点です。これにより、クヌースの引用はさらに適切になります。リスクを冒す前に、最適化が必要であることを証明する必要があります。
グラハム

2
ほとんどのプロセッサー(したがってコンパイラー)は、1回の操作でabs()を実行できます。残念ながら、整数の場合はそうではありません。ARM64は、フラグがすでにから設定されている場合は使用することができ、条件ネゲートを持っているadds、とARMは、逆サブを前提としている(rsblt=逆サブレス-THAの場合)が、他のすべてが実装する複数の追加の命令が必要ですabs(a+b)abs(a)godbolt.org/z/Ok_Conは、x86、ARM、AArch64、PowerPC、MIPS、RISC-V asm出力を示しています。比較を範囲チェックに変換することによってのみ(unsigned)(a+b+999) <= 1998U、gccはPhilの答えのように最適化できます。
ピーター

2
この回答の「改善された」コードはまだ間違っていIsSumInRange(INT_MIN, 0)ます。元のコードが返さfalseれるのはINT_MIN+0 > 1000 || INT_MIN+0 < -1000、しかし、「新しく改良」コードが返されますtrueのでabs(INT_MIN+0) < 1000。(または、一部の言語では、例外がスローされるか、未定義の動作が発生します。ローカルリスティングを確認してください。)
Quuxplusone

16

方法Aと方法B、またはその逆を使用するのが適切な場合

ハードウェアは安価です。プログラマーは高価です。したがって、この質問で2人が無駄にした時間のコストは、おそらくどちらの答えよりもはるかに悪いでしょう。

とにかく、ほとんどの最新のコンパイラーは、ローカル変数をレジスターに最適化する方法を見つけます(スタックスペースを割り当てる代わりに)。そのため、メソッドは実行可能コードの点でおそらく同一です。このため、ほとんどの開発者は、意図を最も明確に伝えるオプションを選択します(実際のコード書く(ROC)を参照)。私の意見では、それは方法Aです。

一方、これが純粋にアカデミックな演習である場合は、方法Cを使用して両方の長所を活用できます。

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}

17
a+=b巧妙なトリックですが、パラメータの混乱がデバッグと保守が非常に困難になる可能性がある私の経験法から、言及する必要があります(答えの残りから暗示されない場合に備えて)。
jrh

1
@jrhに同意します。私はROCの強力な擁護者であり、そのようなことは何でもありません。
ジョン・ウー

3
「ハードウェアは安く、プログラマは高価です。」家電の世界では、その声明は間違っています。数百万台を販売する場合、追加の開発コストに500.000ドルを費やして、ユニットあたりのハードウェアコストを0,10ドル節約することは非常に良い投資です。
バートヴァンインゲンシェ

2
@JohnWu:ifチェックを簡略化しましたが、比較の結果を元に戻すのを忘れていました。あなたの関数は、今戻っているtrueときa + bない範囲で。!条件の外側にa を追加するか(return !(a > 1000 || a < -1000))、!反転テストを配布して、return a <= 1000 && a >= -1000;Orを取得するか、範囲チェックフローをうまく行いますreturn -1000 <= a && a <= 1000;
。– ShadowRanger

1
@JohnWu:エッジケースではまだわずかですが、分散ロジックには<=/ >=ではなく</ が必要です></ >、1000および-1000は範囲外として扱われ、元のコードは範囲内として扱われます)。
ShadowRanger

11

読みやすくするために最適化します。方法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つのことだけを行うが、簡単に推論できる小さなメソッド。

(これは個人的な好みです。ネガティブではなくポジティブテストが好きです。元のコードは実際に値が範囲外ではないかどうかをテストしています。)


5
この。(同様の再コメント:上記のコメントを読みやすくしました)。30年前、1 MB未満のRAMを搭載したマシンで作業していたとき、圧搾性能が必要でした-y2k問題と同様に、未使用の変数と参照など。RAMが256kしかない場合は、すぐに追加されます。現在、複数ギガバイトのRAMを搭載したマシンを扱っているため、数MBのRAM使用量を節約するだけでなく、コードの可読性と保守性は良いトレードではありません。
ivanivan

@ivanivan:「y2kの問題」は本当にメモリに関するものではないと思います。データ入力の観点から、2桁の入力は4桁の入力よりも効率的であり、入力したままに保つことは、他の形式に変換するよりも簡単です。
supercat

10
次に、何が起こっているかを見るために2つの関数をトレースする必要があります。額面どおりに受け取ることはできません。名前から、これらが包括的境界か排他的境界かを判断できないためです。そして、その情報を追加すると、関数の名前はそれを表現するコードよりも長くなります。
ピーター

1
読みやすさを最適化し、小さく、理由がわかりやすい機能を作成します–確かに同意します。しかし、私は強く名前を変更することを同意しないabnumber1してnumber2どのような方法でエイズの可読性。また、関数の命名に一貫性がありません:IsSumInRange範囲IsValueInRangeを引数として受け入れる場合、なぜ範囲をハードコーディングするのですか?
左辺

1番目の関数はオーバーフローする可能性があります。(他の回答のコードと同様。)オーバーフローセーフコードの複雑さは、それを関数に入れるための引数です。
philipxy

6

要するに、この質問は現在のコンピューティングにはあまり関係がないと思いますが、歴史的な観点から見ると、それは興味深い思考の練習です。

あなたのインタビュアーは、Mythical Man Monthのファンです。この本の中で、Fred Brooksは、プログラマーがツールボックスに2つのバージョンの主要な機能が必要であると主張しています:メモリ最適化バージョンとCPU最適化バージョン。フレッドは、マシンに8キロバイトのRAMしか搭載できないIBM System / 360オペレーティングシステムの開発をリードした経験に基づいています。このようなマシンでは、特にコンパイラがそれらを効果的に最適化しない場合(またはコードがアセンブリ言語で直接記述された場合)、関数のローカル変数に必要なメモリが潜在的に重要になる可能性があります。

現在の時代では、メソッド内のローカル変数の有無が目立った違いをもたらすシステムを見つけるのは難しいと思います。変数が重要になるためには、メソッドは再帰的であり、深い再帰が予想されます。それでも、変数自体が問題を引き起こす前に、スタックの深さを超えてスタックオーバーフロー例外が発生する可能性があります。問題になる可能性のある唯一の実際のシナリオは、再帰的な方法でスタックに割り当てられた非常に大きな配列を使用する場合です。しかし、ほとんどの開発者が大規模な配列の不必要なコピーについて二度考えると思うので、それはまたありそうにありません。


4

割り当て後s = a + b; 変数aとbは使用されなくなりました。したがって、完全に脳に損傷を与えたコンパイラを使用していない場合、sにはメモリは使用されません。とにかくaとbに使用されたメモリは再使用されます。

しかし、この機能の最適化はまったくナンセンスです。スペースを節約できる場合、関数の実行中はおそらく8バイトになります(関数が戻ると回復します)ので、まったく意味がありません。時間を節約できれば、1ナノ秒単位になります。これを最適化することは時間の無駄です。


3

ローカル値型変数はスタック上に割り当てられるか、(おそらくこのような小さなコードの場合)プロセッサ内のレジスタを使用し、RAMをまったく見ることができません。いずれにせよ、彼らは短命であり、心配することはありません。潜在的に大規模で長期間存続するコレクション内のデータ要素をバッファリングまたはキューイングする必要がある場合、メモリの使用を検討し始めます。

そして、それはあなたがあなたのアプリケーションのために最も気にするものに依存します。処理速度?反応時間?メモリフットプリント?保守性?設計の一貫性?すべてあなた次第。


4
Nitpicking:.NET(少なくとも投稿の言語は指定されていません)は、ローカル変数が "スタック上"に割り当てられることを保証しません。Eric Lippertによる「スタックは実装の詳細」を参照してください。
jrh

1
@jrhスタックまたはヒープ上のローカル変数は実装の詳細である場合がありますが、誰かが本当にスタック上の変数が必要な場合は、stackallocand now がありますSpan<T>。プロファイリング後のホットスポットで役立つ可能性があります。また、構造体に関するドキュメントのいくつかは、値型スタック上にあり、参照型スタックされないことを暗示してます。とにかく、せいぜい少しのGCを避けるかもしれません。
ボブ

2

他の答えが言ったように、最適化の対象を考える必要があります。

この例では、まともなコンパイラーが両方のメソッドに対して同等のコードを生成するのではないかと考えているため、決定はランタイムメモリーに影響を与えません!

何それはない影響を与えることは、コードの可読性です。(コードは、コンピューターだけでなく人間が読むためのものです。)2つの例の間に大きな違いはありません。他のすべてが等しい場合、簡潔さを美徳と考えるので、おそらくメソッドBを選択するでしょう。しかし、他のすべてのものはめったに等しくありません。

考慮事項:

  • 中間式には副作用がありますか?不純な関数を呼び出したり、変数を更新したりする場合、もちろんそれを複製するのはスタイルだけでなく正確さの問題です。
  • 中間式はどのくらい複雑ですか?大量の計算を実行したり、関数を呼び出したりすると、コンパイラーは最適化できない可能性があり、パフォーマンスに影響します。(ただし、Knuthが言ったように、「わずかな効率については忘れてください。時間の約97%を言うべきです。」)
  • 中間変数には意味がありますか?何が起こっているのかを説明するのに役立つ名前を付けることができますか?短いが有益な名前はコードをよりよく説明できますが、意味のない名前は視覚的なノイズです。
  • 中間式はどれくらいですか?長い場合は、複製するとコードが長くなり読みにくくなる可能性があります(特に強制的に改行する場合)。そうでない場合、複製は全体的に短くなる可能性があります。

1

答えの多くが指摘しているように、この関数を最新のコンパイラで調整しようとしても違いはありません。オプティマイザーは、おそらく最良の解決策を見つけることができます(それを証明するためのアセンブラーコードを示した答えに投票してください!)。あなたは、インタビューのコードは、あなたが比較するように求められたコードではないので、おそらく実際の例はもう少し理にかなっていると述べました。

しかし、この質問をもう一度見てみましょう。これはインタビューの質問です。本当の問題は、あなたが仕事をやりたいと思って、どのように答えるべきでしょうか?

また、インタビュアーが彼らが何について話しているかを知っていて、あなたが知っていることを見ようとしていると仮定しましょう。

オプティマイザーを無視すると、1つ目はスタック上に一時変数を作成しますが、2つ目は作成しませんが、計算を2回実行します。したがって、最初はより多くのメモリを使用しますが、高速です。

とにかく、計算には結果を保存するために一時変数が必要になる可能性があるため(結果を比較するため)、その変数に名前を付けるかどうかは違いをもたらさないかもしれません。

次に、実際にはコードが最適化され、すべての変数がローカルであるため、同等のマシンコードが生成される可能性が高いことに言及します。ただし、使用しているコンパイラに依存します(Javaでローカル変数を「最終」として宣言することで、パフォーマンスの改善が得られるのはそれほど昔ではありませんでした)。

スタックはどのような場合でも独自のメモリページに存在するため、追加の変数によってスタックがページをオーバーフローさせない限り、実際にはそれ以上のメモリは割り当てられません。オーバーフローした場合、まったく新しいページが必要になります。

より現実的な例は、多くの計算結果を保持するためにキャッシュを使用するかどうかの選択である可能性があり、これによりCPUとメモリの問題が発生することに言及します。

これはすべて、あなたが話していることを知っていることを示しています。

代わりに読みやすさに焦点を当てた方が良いと言うために、それを最後に任せます。この場合は真実ですが、インタビューの文脈では、「パフォーマンスについては知りませんが、私のコードはジャネットとジョンの物語のように読みます」と解釈される場合があります。

あなたがすべきではないことは、コードの最適化が必要ではない方法に関する通常の当たり障りのないステートメントを削除することです。コードをプロファイルするまで最適化しないでください(これは、自分にとって悪いコードを見ることができないことを示しています)、ハードウェアはプログラマーよりも安価です、そしてどうぞ、Knuthの「早すぎる何とか...」を引用しないでください。

コードのパフォーマンスは非常に多くの組織で真の問題であり、多くの組織はそれを理解するプログラマーを必要としています。

特に、Amazonなどの組織では、コードの一部に大きな影響力があります。コードスニペットは、数千台のサーバーまたは数百万台のデバイスに展開され、1年のうち毎日何十億回と呼ばれることがあります。同様のスニペットが数千ある場合があります。悪いアルゴリズムと良いアルゴリズムの違いは、簡単に1000倍になります。数字を増やして、これをすべて倍数にしてください。違いが生じます。性能の悪いコードの組織にとって潜在的なコストは、システムが容量を使い果たした場合、非常に大きなものになるか、致命的なものにさえなります。

さらに、これらの組織の多くは競争の激しい環境で働いています。そのため、競合他社のソフトウェアが既に所有しているハードウェアで正常に動作する場合、またはソフトウェアがモバイルハンドセットで実行され、アップグレードできない場合は、単により大きなコンピューターを購入するよう顧客に伝えることはできません。一部のアプリケーションは特にパフォーマンスが重視され(ゲームやモバイルアプリが思い浮かぶ)、応答性や速度に応じて生存または消滅する場合があります。

私は個人的に20年以上にわたり、パフォーマンスの問題のためにシステムが故障したり使用できなくなったりする多くのプロジェクトに取り組んできました。彼らが書いていたものの影響。さらに、これはコードの一部ではなく、常にどこにでもあります。私が現れたとき、それはパフォーマンスについて考えることを遅らせる方法です:ダメージがなされました。

コードのパフォーマンスを理解することは、コードの正確性とコードスタイルを理解するのと同じ方法で得られる優れたスキルです。それは練習から出てきます。パフォーマンス障害は、機能障害と同じくらいひどい場合があります。システムが機能しない場合、機能しません。理由は関係ありません。同様に、決して使用されないパフォーマンスと機能は両方とも悪いです。

そのため、インタビュアーがパフォーマンスについて質問した場合は、できるだけ多くの知識を試してみることをお勧めします。質問が悪いと思われる場合は、その場合に問題ではないと思う理由を丁寧に指摘してください。クヌースを引用しないでください。


0

最初に正確さのために最適化する必要があります。

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」まで加算されます)。

インタビュアーが何を探していたかは、彼または彼女がチャイムを鳴らさない限りわかりませんが、「オーバーフローは本当です」

インタビューの状況では、問題の範囲を明確にするために質問をしてください。許容される最大および最小の入力値は何ですか?これらを取得したら、呼び出し元が範囲外の値を送信した場合に例外をスローできます。または(C#では)チェック済みの{}セクションを使用できます。これにより、オーバーフロー時に例外がスローされます。はい、それはより多くの作業と複雑ですが、時にはそれがかかるものです。


メソッドは単なる例です。それらは正しいと書かれているのではなく、実際の質問を説明するために書かれています。入力をありがとう!
コーリーP

インタビューの質問はパフォーマンスに向けられていると思うので、質問の意図に答える必要があります。インタビュアーは、限界での行動について尋ねていません。とにかく面白いサイドポイント。
rghome

1
@Corey Goodインタビュアーは、1)問題に関する候補者の能力を評価します。これは、ここでrghomeによってさらに示唆されています。後のキャリアインタビューで-幸運を祈ります。
chux

0

あなたの質問は「これを最適化する必要がありますか?」です。

バージョンAとバージョンBは、Aを優先させる重要な詳細が1つ異なりますが、最適化とは無関係です。コードを繰り返さないでください。

実際の「最適化」は共通部分式の除去と呼ばれ、ほとんどすべてのコンパイラーが行います。最適化がオフになっている場合でも、この基本的な最適化を行う人もいます。したがって、それは本当に最適化ではありません(生成されたコードはほぼ確実にすべての場合でまったく同じになります)。

しかし、それ最適化でない場合、なぜそれが好ましいのでしょうか?さて、気にしないでコードを繰り返さないでください!

まず、条件句の半分を誤って間違ってしまうリスクはありません。しかし、もっと重要なのは、このコードを読んでいる人が、経験ではなく、あなたがやろうとしていることをすぐに理解できることですif((((wtf||is||this||longexpression))))。読者が目にするのif(one || theother)は、良いことです。ない稀に、私はそれを起こらないあなたが他の人は3年後に独自のコードを読んで、「WTFこれが意味するのでしょうか?」を考えていることです。その場合、コードが意図が何であるかを即座に伝えることができれば、常に役立ちます。共通の部分式に適切な名前が付けられている場合は、そうです。
また、将来いつでも、たとえばに変更a+bする必要があると判断したa-b場合は、1つを変更する必要があります場所ではなく、2。そして、偶然に二度目の間違いを犯す危険性はありません。

あなたの実際の質問、何のために最適化すべきかについて、まずあなたのコードは正しいはずです。これは絶対に最も重要なことです。正しくないコードは悪いコードです。さらに、間違っているにもかかわらず「うまく動作する」場合、または少なくとも正常に動作するよう見える場合でもです。その後、コードは読み取り可能になります(コードに不慣れな人が読み取り可能)。
...最も効率の悪いものではありません)。

しかし、ほとんどのアプリケーションでは、ほとんどの場合、最適化コンパイラーを介して適切なアルゴリズムを使用して、読み取り可能な適切なコードを実行した後のパフォーマンスは問題ありません。実際に心配する必要はありません。

そうでない場合、つまり、アプリケーションのパフォーマンスが実際に要件を満たしていない場合、その場合にのみ、試みたようなローカルな最適化を行うことを心配する必要があります。ただし、できればトップレベルのアルゴリズムを再検討することをお勧めします。アルゴリズムが優れているために50,000回ではなく500回関数を呼び出すと、マイクロ最適化で3クロックサイクルを節約するよりも大きな影響があります。ランダムメモリアクセスで常に数百サイクル停止しない場合、これはいくつかの安価な計算などを行うよりも大きな影響があります。

最適化は難しい問題です(それについては本全体を書いて終わりはありません)。そして、特定の場所を盲目的に最適化することに時間を費やすことは(それがボトルネックであるかどうかさえ知らずに!)通常、無駄な時間です。プロファイリングがなければ、最適化を正しく行うのは非常に困難です。

しかし、経験則として、あなたが盲目に飛んでい何かをする必要がある/したいとき、または一般的なデフォルト戦略として、「メモリ」のために最適化することをお勧めします。
「メモリ」(特に空間的局所性とアクセスパターン)の最適化は、すべてが「まったく同じ」であったかつてとは異なり、今日ではRAMへのアクセスが最も高価なものの1つであるため、通常は利点があります(ディスクからの読み取りが不足しています!)あなたが原則としてできること。一方、ALUは安価で、毎週高速になっています。メモリの帯域幅と遅延は、ほとんど同じ速度で改善されません。優れたローカリティと優れたアクセスパターンは、データ量の多いアプリケーションでの悪いアクセスパターンと比較して、実行時に5倍(極端な例では20倍)の違いを簡単に生じます。あなたのキャッシュに親切にしてください、そしてあなたは幸せな人になります。

前の段落を概観するには、実行できるさまざまなことのコストを考慮します。以下のようなものを実行すると、a+b(アウト最適化されていない場合)は、1つのまたは2つのサイクルがかかりますが、CPUは通常、サイクルごとに複数の命令を開始することができ、パイプライン非依存の命令はそうより現実的にそれだけであなたの半サイクル以下の周りに何かをコストすることができます。理想的には、コンパイラーがスケジューリングに長けていて、状況によってはコストがゼロになる場合があります。
データ(「メモリ」)の取得には、ラッキーでL1の場合は4〜5サイクル、ラッキーでない場合(L2ヒット)は約15サイクルかかります。データがキャッシュにまったくない場合、数百サイクルかかります。行き当たりばったりのアクセスパターンがTLBの能力を超えている場合(〜50エントリで簡単に実行できる場合)、さらに数百サイクルを追加します。偶然のアクセスパターンが実際にページフォールトを引き起こす場合、最良の場合は数万サイクル、最悪の場合は数百万サイクルかかります。
今考えてみて、あなたが最も緊急に避けたいことは何ですか?


0

メソッドのメモリとパフォーマンスの速度を最適化するタイミング

最初に機能を正しく取得した後。次に、選択性は、マイクロ最適化に関係します。


最適化に関するインタビューの質問として、コードは通常の議論を引き起こしますが、コードは機能的に正しいというより高いレベルの目標を逃しますか?

C ++とCおよびその他の両方intは、オーバーフローをからの問題と見なしa + bます。それは十分に定義されておらず、Cはそれを未定義の動作と呼びます。「ラップ」は指定されていません-それが一般的な動作であっても。

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

呼び出されるこのような関数IsSumInRange()は、のすべてのint値に対して適切に定義され、正しく実行されることが期待されますa,b。生でa + bはありません。ACソリューションは以下を使用できます。

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

上記のコードはint、以下のように、可能であればテストよりも広い整数型を使用するか、ロジック内sum > Nsum < -Nテストを分散することで最適化できますif (a >= 0)。しかし、そのような最適化は、スマートコンパイラを与えられた場合に「高速な」出力コードを実際にもたらさず、賢いという余分なメンテナンスの価値はありません。

  long long sum a;
  sum += b;

使用abs(sum)する場合でも問題が発生しやすいsum == INT_MIN


0

私たちはどのようなコンパイラについて話し、どのような「メモリ」ですか?あなたの例では、合理的なオプティマイザを想定しているため、そのa+bような算術を行う前に、一般的に式をレジスタ(メモリの形式)に保存する必要があります。

したがって、a+b2回遭遇するダムコンパイラについて話している場合、2番目の例ではより多くのレジスタ(メモリ)を割り当てます。最初の例では、ローカル変数にマッピングされた1つのレジスタにその式を1回格納するだけですあなたがスタック愚かなコンパイラの別のタイプで作業している場合を除き「再、その場合には、すべての場所ですべての単一の変数をこぼし...この時点で非常に愚かなコンパイラの話を多分最初のものはそれをより最適化するために、多くの悲しみを引き起こします二番目*。

私はまだそれをスクラッチしたい2つ目は、それがために3つのレジスタを割り当てる終わるかもしれないので、それはこぼれを積層するがちだ場合でも、ダムコンパイラでより多くのメモリを使用する可能性があると思うa+bし、流出aし、bより多くの。最も原始的なオプティマイザについて話している場合、キャプチャa+bするsことで、レジスタ/スタックの使用量が少なくなります。

これは、測定/分解がなくばかげた方法で非常に投機的であり、最悪のシナリオでも、これは「メモリとパフォーマンス」のケースではありません(考えられる最悪のオプティマイザの間でさえ、私たちは話していませんスタック/レジスタなどの一時メモリ以外)については、純粋に「パフォーマンス」のケースであり、合理的なオプティマイザの中で2つは同等であり、合理的なオプティマイザを使用していない場合、最適化に取りつかれている理由特に不在測定?それは、命令選択/レジスタ割り当てのアセンブリレベルのフォーカスに似ています。たとえば、すべてがスタックするインタープリターを使用する場合、生産性を維持するために探している人はいないでしょう。

メソッドのメモリとパフォーマンスの速度を最適化するタイミング

この質問について、私がもっと広く取り組むことができれば、正反対の2つが見つからないことがよくあります。特に、アクセスパターンがシーケンシャルであり、CPUキャッシュの速度が与えられている場合、非自明な入力でシーケンシャルに処理されるバイト量の削減は、多くの場合、そのデータをより高速に処理することに変換されます。もちろん、データがはるかに小さく、かなり多くの命令と引き換えに、より少ない命令と引き換えに、より大きな形式で順次処理する方が高速である可能性があるブレークポイントがあります。

しかし、多くの開発者は、これらのタイプのケースでのメモリ使用量の削減が、処理に費やされる時間の比例した削減にどの程度変換できるかを過小評価する傾向があることを発見しました。メモリアクセスではなくパフォーマンスコストを命令に変換することは、小さな計算をスピードアップしようとしても無駄な大きなLUTに到達するポイントにアクセスすることは非常に直感的であり、追加のメモリアクセスによってパフォーマンスが低下することがわかります。

巨大な配列(例のようにローカルスカラー変数を使用しない)を介したシーケンシャルアクセスの場合、シーケンシャルに処理するメモリが少なくなることでパフォーマンスが向上し、特に結果のコードが他の方法よりも単純な場合は、 't、私の測定値とプロファイラーがそうでないことを教えてくれるまで、そしてそれは重要です、同じ方法で、ディスク上の小さなバイナリファイルを連続して読み取ることは、大きなファイルよりも速くすり抜けるだろうと仮定します)、その仮定が私の測定に適用されなくなることが示されるまで。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.