就職の面接で、「C++では、通常の変数識別子またはポインターを使用して、どのように変数に速くアクセスするのですか」という質問がありました。私はその質問に対する技術的な答えがよくなかったと言わざるを得ないので、私は乱暴な推測をしました。
アクセス時間は通常の変数/識別子がポインタと同じように値が格納されているメモリアドレスへのポインタと同じであろうと言いました。言い換えると、速度の点で、両方とも同じパフォーマンスであり、ポインタが指すメモリアドレスを指定できるため、ポインタが異なるだけです。
インタビュアーは私の答えに非常に確信/満足しているようには見えませんでした(彼は何も言わず、ただ何かを求め続けただけです)、それで私はSO'ersに私の答えが正しいかどうか尋ねましたが、そうでない場合は(から理論と技術的なPOV)。
変数はメインメモリに存在する必要はありません。状況に応じて、コンパイラーはその存続期間の全体または一部にわたってレジスターにそれを保管でき、レジスターへのアクセスはRAMへのアクセスよりもはるかに高速です。
「変数」にアクセスするときは、アドレスを検索して、値をフェッチします。
覚えておいてください-ポインタIS変数です。実際、あなたは:
a)(ポインタ変数の)アドレスを検索し、
b)値(その変数に格納されているアドレス)をフェッチする
... その後 ...
c)指し示されたアドレスで値をフェッチします。
したがって、「直接」ではなく「ポインタ」を介してアクセスする場合、(少し)追加の作業と(わずかに)より長い時間がかかります。
ポインター変数(CまたはC++)でも参照変数(C++のみ)でも、まったく同じことが起こります。
しかし、違いは非常に小さいです。
少しの間、最適化を無視して、ローカル変数と(ローカル)ポインタを通じて変数を参照するために、抽象マシンが何をしなければならないかを考えてみましょう。ローカル変数が次のように宣言されている場合:
int i;
int *p;
iの値を参照するとき、最適化されていないコードは、現在のスタックポインターを(たとえば)12過ぎた値にして、それをレジスターにロードして、操作できるようにする必要があります。一方、* pを参照する場合、同じ最適化されていないコードは、現在のスタックポインターを過ぎて16からpの値を取得し、それをレジスターにロードしてから、レジスターが指す値を取得して別のレジスターにロードする必要があります。以前と同じように作業できます。作業の最初の部分は同じですが、ポインターアクセスには概念的に、値を操作する前に実行する必要がある追加の手順が含まれます。
それが、インタビューの質問のポイントだったと思います。2種類のアクセスの基本的な違いを理解しているかどうかを確認します。ローカル変数アクセスには一種のルックアップが含まれると考えていましたが、ポインターアクセスには、ポインターの値を取得する前に、ポインターの値と同じタイプのルックアップが含まれています。 。単純で最適化されていない言葉で言えば、その余分なステップのために、ポインターアクセスは遅くなります。
最適化を行うと、2つの時間が非常に近いかまったく同じになることがあります。他の最近のコードがすでにpの値を使用して別の値を参照している場合、レジスターでpがすでに見つかっている可能性があるため、pを介した* pのルックアップはスタックを介したiのルックアップと同じ時間を要しますポインタ。同じことを言えば、最近iの値を使用した場合は、レジスタにitがすでにある場合があります。また、* pの値についても同じことが言えますが、オプティマイザーは、pがその間に変更されていないことが確実な場合にのみ、レジスターからその値を再利用できます。 iの値を再利用するような問題はありません。つまり、最適化では両方の値へのアクセスに同じ時間がかかる可能性がありますが、ローカル変数へのアクセスが遅くなることはほとんどなく(実際に異常な場合を除いて)、非常に速くなる可能性があります。これは、インタビュアーの質問に対する正しい答えになります。
メモリ階層が存在する場合、時間の違いがさらに顕著になることがあります。ローカル変数はスタック上で互いに近くに配置されます。つまり、最初にアクセスしたときに、メインメモリとキャッシュで必要なアドレスが見つかる可能性が非常に高くなります(最初のローカル変数でない限り)このルーチンでアクセスします)。ポインタが指すアドレスにはそのような保証はありません。最近アクセスされていない限り、ポイントされたアドレスにアクセスするために、キャッシュミスまたはページフォールトさえ待たなければならない場合があります。これにより、ローカル変数よりも桁違いに遅くなる可能性があります。いいえ、それは常に発生するわけではありませんが、場合によっては違いが生じる可能性のある潜在的な要因であり、それも候補者がそのような質問に応じて持ち出すことができるものです。
次に、他のコメンターが提起した質問についてはどうですか?確かに、1回のアクセスでは、砂粒のように絶対的な違いはごくわずかです。しかし、砂を十分にまとめると、ビーチになります。そして、(比喩を続けるために)ビーチの道を素早く走れる人を探しているなら、彼または彼女が走り始める前に、砂粒をすべて道路から一掃することにこだわる人を望まないでしょう。彼または彼女が不必要に膝の深い砂丘を走っているときに気づく誰かが欲しいのですが。プロファイラーは常にここであなたを救うわけではありません-これらの比喩的な言葉で、彼らはあなたが行き詰まっている砂の小さな粒の多くに気づくよりも、走り回る必要がある単一の大きな岩を認識するのにはるかに優れています。ですから、私のチームのメンバーが基本的なレベルでこれらの問題を理解していれば、その知識を使うことがめったになくなることはありません。マイクロ最適化の追求で明確なコードを書くのをやめないでください。ただし、特にデータ構造を設計するときは、パフォーマンスを犠牲にする可能性のある種類のものに注意し、支払っている価格で十分な価値を得ているかどうかを把握してください。 。候補者がこれらの問題について理解を深めるために、これが面接の質問として合理的だったと思うのです。
Paulsm4とLaCの発言+少しasm:
int y = 0; mov dword ptr [y]、0 y = x; mov eax、dword ptr [x]; xをフェッチして登録します mov dword ptr [y]、eax; y に保存します。y = * px; mov eax、dword ptr [px]; xのアドレスを取得 mov ecx、dword ptr [eax]; x mov dword ptr [y]、ecxをフェッチします。 y に保存します
それはそれほど重要ではありませんが、これはおそらく最適化するのがおそらく難しいです(ポインタはメモリ内のどこかの場所を指しているだけなので、CPUレジスタに値を保持できません)。したがって、y = xに対して最適化されたコードです。次のようになります:
mov dword ptr [y], ebx
-ローカル変数xがebx
に格納されていると仮定した場合
面接官はあなたに単語registerについて言及してほしいと思っていたと思います。のように、変数をレジスタ変数として宣言すると、コンパイラはCPUのレジスタに確実に格納されるように全力を尽くします。
バスへのアクセスや、他のタイプの変数やポインターのネゴシエーションについてのちょっとしたチャットは、それを組み立てるのに役立ちました。
paulsm4とLaCはすでに他のメンバーと一緒にうまく説明しています。ポインターがページアウトされたヒープ内の何かを指しているときのページングの効果を強調したいと思います。
=>ローカル変数は、スタックまたはレジスタのいずれかで使用できます
=>ポインターの場合、ポインターがキャッシュにないアドレスを指している可能性があり、ページングにより速度が確実に低下します。
質問の重要な部分は「変数へのアクセス」だと思います。私にとって、変数がスコープ内にある場合、それにアクセスするための変数(または参照)へのポインターを作成するのはなぜですか?ポインタまたは参照を使用しても意味があるのは、変数自体が何らかのデータ構造である場合、または非標準的な方法(intをfloatとして解釈するなど)でアクセスする場合のみです。
ポインターまたは参照を使用すると、非常に特定の状況でのみ高速になります。一般的な状況では、最適化に関する限り、コンパイラを推測し直そうとしているように思えますが、私の経験では、何をしているのかを知らない限り、それは悪い考えです。
キーワードにも依存します。 constキーワードは、コンパイル時に変数が完全に最適化されることを意味します。これはポインタよりも高速です。レジスタキーワード 保証はありません 変数がレジスタに格納されていること。それで、あなたはそれがより速いかどうかをどうやって知るのですか?答えは、1つのサイズですべての答えを収めることはできないため、状況によって異なると思います。
変数は特定のタイプの値を保持し、変数にアクセスすることは、メモリまたはレジスタからこの値を取得することを意味します。メモリから値を取得するときは、どこかからアドレスを取得する必要があります。ほとんどの場合、レジスタにロードする必要があります(ロードコマンド自体の一部である場合もありますが、これは非常にまれです)。
ポインターは値のアドレスを保持します。この値はメモリ内にある必要があります。ポインタ自体はメモリ内またはレジスタ内にあります。
ポインタを介した平均的なアクセスは、変数を介した値へのアクセスよりも遅くなると思います。
分析では、ポインター自体がメモリ変数であり、これもアクセスする必要があるという一般的なシナリオは無視されます。
ソフトウェアのパフォーマンスに影響を与える要因は多数ありますが、関係する変数について特定の単純化の仮定を行うと(特に、変数がキャッシュされない場合)、ポインターの間接参照の各レベルで追加のメモリアクセスが必要になります。
int a = 1234; // luggage combination
int *b = &a;
int **c = &b;
...
int e = a; // one memory access
int e = *b; // two memory accesses
int e = **c; // three memory accesses
したがって、「どちらが速いか」に対する短い答えは、発生している可能性のあるコンパイラとプロセッサの最適化を無視して、変数に直接アクセスする方が速いということです。
このコードがタイトループで繰り返し実行される最良のシナリオでは、ポインター値はCPUレジスターにキャッシュされるか、最悪の場合はプロセッサーのL1キャッシュにキャッシュされます。そのような場合、「直接」はおそらく「スタックポインター」レジスター(およびいくつかのオフセット)を介して意味するため、第1レベルポインターの間接指定は、変数に直接アクセスするよりも高速または高速である可能性があります。どちらの場合も、値へのポインタとしてCPUレジスタを使用しています。
変数のアドレスが命令ストリームにハードコード化されているグローバルデータや静的データなど、この分析に影響を与える可能性のある他のシナリオがあります。そのようなシナリオでは、答えは、関係するプロセッサの詳細に依存する場合があります。
より良い答えは、ポインターが「指している」場所に依存することかもしれません。変数がすでにキャッシュにある可能性があることに注意してください。ただし、ポインタはフェッチペナルティを招く可能性があります。これは、リンクリストとベクターのパフォーマンスのトレードオフに似ています。すべてのメモリが連続しているため、ベクターはキャッシュフレンドリーです。ただし、リンクリストにはポインタが含まれているため、メモリが場所全体に分散している可能性があるため、キャッシュペナルティが発生する可能性があります。