ポインターの逆参照操作を実行するのにどれくらいの費用がかかりますか?
メモリ転送がオブジェクトサイズにある程度比例していることは想像できますが、逆参照操作の部分がどれほど高価かを知りたいです。
逆参照は、マシンコードに変換された場合、逆参照されたオブジェクトをどのように処理するかによって異なる意味を持つ可能性があります。ポインタを介してクラスの単一のメンバーにアクセスすることは、通常は安価です。たとえば、cがint
メンバーnを持つclass C
のインスタンスへのポインターである場合、次のようになります。
int n = c->n;
1つまたは2つの機械語命令に変換される場合があり、1回のメモリアクセスでレジスタをロードする場合があります。
一方、これはcが指すオブジェクトの完全なコピーを作成することを意味します。
C d = *c;
このコストはCのサイズによって異なりますが、主要な費用はコピーであり、「逆参照」の部分は実際にはコピー命令でポインタアドレスを単に「使用」していることに注意してください。
ラージオブジェクトのメンバーにアクセスするには、通常、オブジェクトがローカルオブジェクトであるかどうかに関係なく、ポインターオフセットの計算とメモリアクセスが必要です。通常、非常に小さなオブジェクトのみがレジスターにのみ存在するように最適化されます。
参照に対するポインタのコストが心配な場合は、心配しないでください。これらの違いは言語のセマンティクスの違いであり、マシンコードが生成されるまでに、ポインターと参照アクセスはまったく同じに見えます。
これは、逆参照されたポインターをどのように使用するかによって異なります。単なる逆参照操作は、それ自体では何もしません。ポインタがT*
の場合、オブジェクトを表すT
型の左辺値を取得するだけです。
struct a {
int big[42];
};
void f(a * t) {
// does nothing. Only interesting for standard or compiler writers.
// it just binds the lvalue to a reference t1.
a & t1 = *t;
}
逆参照操作によって返された左辺値によって示されるオブジェクトから実際に値を取得する場合、コンパイラーはオブジェクトに含まれるデータをコピーする必要があります。単純なPODの場合、これは単なるmemcpy
です。
a aGlobalA;
void f(a * t) {
// gets the value of of the object denoted by *t, copying it into aGlobalA
aGlobalA = *t;
}
私のgccポートはこのコードをfに出力します:
sub $29, $29, 24 ; subtract stack-pointer, creating this frame
stw $31, $29, 20 ; save return address
add $5, $0, $4 ; copy pointer t into $5 (src)
add $4, $0, aGlobalA ; load address of aGlobalA into $4 (dst)
add $6, $0, 168 ; put size (168 bytes) as 3rd argument
jal memcpy ; call memcpy
ldw $31, $29, 20 ; restore return address
add $29, $29, 24 ; add stack-pointer, destroying this frame
jr $31
最適化されたマシンコードはmemcpy
の呼び出しの代わりにインラインコードを使用しますが、これは実際には単なる実装の詳細です。重要なのは、単に*t
がコードを実行していないということですが、そのオブジェクトの値にアクセスするには、実際にコピーする必要があります。
ユーザー定義のコピー割り当て演算子を持つ型を使用する必要がある場合、事柄はより複雑になります。
struct a {
int big[42];
void operator=(a const&) { }
};
同じ関数f
のコードは次のようになります。
sub $29, $29, 8
add $29, $29, 8
jr $31
ああ。でも、そんなに驚きではなかったのでは?結局のところ、コンパイラはoperator=
を呼び出すことになっています。何もしない場合、関数全体も何もしません。
私たちが導き出すことができる結論は、すべてのoperator*
の戻り値がどのように使用されるかに依存すると思います。逆参照するポインタだけがある場合、生成されたコードは状況に大きく依存することがわかります。 operator*
をオーバーロードしたクラス型を逆参照した場合の動作は示していません。しかし、基本的には、operator=
で見たような動作です。すべての測定は-O2
で行われたため、コンパイラーは呼び出しを適切にインライン化しました:)
通常のシステムでポインターを逆参照する際の最も重要な要素は、キャッシュミスが発生する可能性が高いことです。 SDRAMメモリでのランダムアクセスには数十ナノ秒(64など)かかります。ギガヘルツプロセッサの場合、これは、プロセッサが数百(または>千)サイクルのアイドル状態にあり、その間に他に何も実行できないことを意味します。
SRAMベースのシステム(組み込みソフトウェアでのみ見つかる)、またはソフトウェアがキャッシュ最適化されている場合にのみ、他の投稿で説明されている要素が機能します。
デリファレンスは、メモリからデータをフェッチするための命令を必要とするため、コストが高くなる可能性があります。その場合、プロセッサはキャッシュされていないメモリから、さらにはハードディスクからデータをフェッチする必要があります(ハードページフォールトの場合)。
逆参照(複数)はCPUサイクルを要します。
書く代わりに:
string name = first->next->next->next->name;
int age = first->next->next->next->age;
this is O(n)
それを次のように書いてください:
node* billy_block = first->next->next->next;
string name = billy_block->name;
int age = billy_block->age;
this is O(1)
したがって、コードは4番目のブロックに到達するためだけにすべてのブロックを「尋ねる」ことはありません。
複数の逆参照は、隣の隣人だけを知っている近所があるようなものです。
友達のビリーが住んでいる最初のブロックの人に尋ねると、彼は彼にあなたの友達を知らないと言って、彼は彼らの隣の隣人だけを知っていると言って、それから彼はあなたに言うだけだと想像してみてください彼の隣人に尋ねるなら、あなたは彼の隣人に尋ねます、彼は最初のブロックがしたのと同じことを答えます、あなたはあなたの友人のブロックに到着するまで尋ね続けます。あまり効率的ではない
ポインターの逆参照は、アドレスを(アドレス)レジスターにコピーする以上のものではありません。それで全部です。