web-dev-qa-db-ja.com

ポインタ変数の使用はメモリのオーバーヘッドではありませんか?

CやC++などの言語では、変数へのポインターを使用している間、そのアドレスを格納するためにもう1つのメモリ位置が必要です。これはメモリのオーバーヘッドではありませんか?これはどのように補償されますか?ポインタは、タイムクリティカルな低メモリアプリケーションで使用されますか?

28
Sudip Bhandari

実際、オーバーヘッドは、ポインタを格納するために必要な追加の4バイトまたは8バイトに実際にはありません。ほとんどの場合、ポインタは動的メモリ割り当てに使用されます。つまり、メモリブロックを割り当てる関数を呼び出し、この関数は、そのメモリブロックを指すポインタを返します。この新しいブロック自体は、かなりのオーバーヘッドを表しています。

今、あなたはしませんする必要がありますポインタを使用するためにメモリ割り当てに従事する:静的にまたはスタックで宣言されたintの配列を持つことができ、 intsにアクセスするためのインデックスの代わりにポインターを使用すると、すべて非常に簡単で効率的です。メモリの割り当ては必要ありません。また、ポインタは通常、整数インデックスの場合とまったく同じだけメモリ内のスペースを占有します。

また、ジョシュアテイラーがコメントで私たちに思い出させるように、ポインターは参照によって何かを渡すために使用されます。たとえば、struct foo f; init_foo(&f);はスタックにfを割り当て、そのstructへのポインタを使用してinit_foo()を呼び出します。それは非常に一般的です。 (これらのポインターを「上向き」に渡さないように注意してください。)C++では、「参照」(foo&)ポインタの代わりに、参照は変更できないポインタにすぎず、同じ量のメモリを占有します。

しかし、ポインタが使用される主な理由は動的メモリ割り当てのためであり、これは他の方法では解決できなかった問題を解決するために行われます。次に、単純な例を示します。ファイルの内容全体を読み取りたいと想像してください。どこに保管しますか?固定サイズのバッファーを使用すると、そのバッファーよりも長くないファイルのみを読み取ることができます。ただし、メモリ割り当てを使用することで、ファイルを読み取るために必要なだけのメモリを割り当ててから、ファイルを読み取ることができます。

また、C++はオブジェクト指向言語であり、OOPのようにabstractionのように、ポインタを使用してのみ実現可能です。JavaとC#make extensiveポインタを使用しているため、ポインタを直接操作することはできません。ポインタを使用して危険なことをするのを防ぐためですが、これらの言語は舞台裏ではすべてがポインターを使用して行われていることに気付いて初めて意味を持ち始めます。

したがって、ポインタはnotはタイムクリティカルな低メモリアプリケーションでのみ使用され、everywhereで使用されます。

33
Mike Nakis

これはメモリのオーバーヘッドではありませんか?

確かに、追加のアドレス(通常、プロセッサによっては4/8バイト)。

これはどのように補償されますか?

そうではない。ポインターに必要な間接参照が必要な場合は、それに対して料金を支払う必要があります。

ポインタは、タイムクリティカルな低メモリアプリケーションで使用されますか?

私はそこであまり仕事をしていませんが、そうだと思います。ポインターアクセスは、アセンブリプログラミングの基本的な側面です。それは取るに足らない量のメモリを必要とし、ポインタ操作はこれらの種類のアプリケーションのコンテキストでさえ高速です。

35
Telastyn

これについては、Telastynと同じように考えていません。

組み込みプロセッサのシステムグローバルは、特定のハードコードされたアドレスでアドレス指定される場合があります。

プログラム内のグローバルは、グローバルとスタティックが格納されているメモリ内の場所を指す特別なポインターからのオフセットとしてアドレス指定されます。

ローカル変数は、関数を入力すると表示され、「フレームポインター」と呼ばれる別の特別なポインターからのオフセットとしてアドレス指定されます。これには、関数への引数が含まれます。スタックポインターでのプッシュとポップに注意する場合は、フレームポインターを取り除き、スタックポインターから直接ローカル変数にアクセスできます。

そのため、配列内を移動する場合でも、目立たないローカル変数やグローバル変数を取得する場合でも、ポインターの間接参照に料金がかかります。それは、変数の種類に応じて、異なるポインターに基づいています。うまくコンパイルされたコードは、そのポインタを使用するたびに再ロードするのではなく、CPUレジスタに保持します。

はい、もちろん。しかし、それはバランスをとる行為です。

低メモリアプリケーションは、通常、いくつかのポインタ変数のオーバーヘッドと大規模プログラム(オーバーヘッドがメモリに格納される必要がある)のオーバーヘッドとのトレードオフを考慮して構築されます。 )ポインターを使用できなかった場合。

この考慮事項はallプログラムに適用されます。左右の中央に複製されたコードが必要以上に大きく、維持不可能な混乱を引き起こしたがらないためです。

CやC++などの言語では、変数へのポインターを使用している間、そのアドレスを格納するためにもう1つのメモリ位置が必要です。これはメモリのオーバーヘッドではありませんか?

ポインタを格納する必要があると想定します。常にそうであるとは限りません。すべての変数は、いくつかのメモリアドレスに格納されます。 longが_long n = 5L;_として宣言されているとします。これにより、nのストレージが特定のアドレスに割り当てられます。そのアドレスを使用して、nの一部を操作するために*((char *) &n) = (char) 0xFF;などの空想的なことを行うことができます。 nのアドレスは、余分なオーバーヘッドとしてどこにも保存されません。

これはどのように補償されますか?

ポインターが明示的に(たとえば、リストなどのデータ構造に)格納されている場合でも、結果のデータ構造は、多くの場合、ポインターのない同等のデータ構造よりもエレガント(シンプル、理解しやすく、扱いやすいなど)です。

ポインタは、タイムクリティカルな低メモリアプリケーションで使用されますか?

はい。マイクロコントローラを使用するデバイスは、多くの場合、ほとんどメモリを搭載していませんが、ファームウェアは、割り込みベクトルの処理やバッファ管理などにポインタを使用する場合があります。

5
Lawrence

ポインターを使用するとオーバーヘッドが確実に消費されますが、利点もわかります。ポインターはインデックスのようなものです。 Cでは、ポインタのみのため、文字列や構造などの複雑なデータ構造を使用できます。

実際には、構造体全体を複製してそれらの間の変更を同期させるのではなく、参照によって変数を渡し、その後ポインタを維持するのが簡単だと仮定します(それらをコピーする場合でも、ポインタが必要になります)。ポインターのない非連続のメモリ割り当てと割り当て解除をどのように処理しますか?

通常の変数であっても、変数が指しているアドレスを格納するシンボルテーブルにエントリがあります。したがって、メモリの点でオーバーヘッドが大きくなるとは思いません(ちょうど4バイトまたは8バイト)。 Java内部でポインタを使用する(参照))などの言語でも、JVMの安全性が低下するため、ポインタを操作することはできません。

ポインタを使用する必要があるのは、データ型の欠落、構造体(c)などの他に選択肢がない場合のみです。ポインタを使用すると、適切に処理されない場合にエラーが発生し、デバッグが比較的困難になるためです。

5
Krrish Raj

では、これはメモリのオーバーヘッドではありませんか?

そうですね?

マシンのメモリアドレス範囲と、スタックに関連付けられない方法でメモリ内の場所を永続的に追跡する必要があるソフトウェアを想像するので、これは厄介な質問です。

たとえば、ユーザーが別の音楽ファイルを読み込もうとしたときに、ユーザーがボタンPushに音楽ファイルをロードし、揮発性メモリからアンロードする音楽プレーヤーを想像してください。

オーディオデータが保存されている場所を追跡するにはどうすればよいですか?メモリアドレスが必要です。プログラムは、メモリ内のオーディオデータチャンクを追跡するだけでなく、メモリ内の場所も追跡する必要があります。したがって、メモリアドレス(つまり、ポインタ)を保持する必要があります。また、メモリアドレスに必要なストレージのサイズは、マシンのアドレス範囲と一致します(例:64ビットアドレス範囲の64ビットポインター)。

つまり、「はい」のようなもので、メモリアドレスを追跡するためにストレージが必要ですが、この種類の動的に割り当てられたメモリの場合は回避できません。

これはどのように補正されますか?

ポインタ自体のサイズについてだけ言えば、スタックを利用することで、場合によってはコストを回避できます。その場合、コンパイラーは、相対メモリー・アドレスを効果的にハードコーディングする命令を生成して、ポインターのコストを回避できます。しかし、これは、大規模な可変サイズの割り当てに対してこれを行うと、スタックオーバーフローに対して脆弱になります。また、(オーディオの例のように)ユーザー入力によって駆動される複雑な一連のブランチに対して行うことは(まったく不可能ではないにしても)非現実的である傾向があります。上記)。

別の方法は、より連続したデータ構造を使用することです。たとえば、ノードごとに2つのポインタを必要とする二重リンクリストの代わりに、配列ベースのシーケンスを使用できます。 N要素の隣接するすべてのグループ間のポインタのみを格納する展開されたリストのように、これら2つのハイブリッドを使用することもできます。

ポインタはタイムクリティカルな低メモリアプリケーションで使用されていますか?

はい、非常に一般的です。多くのパフォーマンス重視のアプリケーションはCまたはC++で記述されており、ポインターの使用が主流です(これらはスマートポインターやstd::vectorstd::stringのようなコンテナーの背後にある可能性がありますが、基礎となるメカニズムは、動的メモリブロックへのアドレスを追跡するために使用されるポインタに要約されます。

ここでこの質問に戻ります。

これはどのように補正されますか?(パート2)

ポインターは通常、100万個(64ビットマシンでは8メガバイトといっても大容量)のように保存している場合を除いて、非常に安価です。

*ベンが「わずかに」8 MBは依然としてL3キャッシュのサイズであることを指摘したので注意してください。ここでは、DRAMの総使用量と、ポインタの正常な使用が指すメモリチャンクに対する一般的な相対サイズという意味で、「ある程度」多く使用しました。

ポインタが高価になるのは、ポインタ自体ではなく、次のとおりです。

  1. 動的メモリ割り当て。動的メモリ割り当ては、基になるデータ構造(例:バディまたはスラブアロケータ)を経由する必要があるため、コストがかかる傾向があります。これらはしばしば死に最適化されていますが、汎用的であり、可変サイズのブロックを処理するように設計されているため、「検索」に似た少なくとも少しの作業(軽量で、場合によっては一定時間)を実行する必要があります。メモリ内の連続するページの空きセットを見つけます。

  2. メモリアクセス。これは、気になるオーバーヘッドが大きくなる傾向があります。初めて動的に割り当てられたメモリにアクセスするときは常に、強制的なページフォールトが発生するだけでなく、キャッシュミスによってメモリがメモリ階層の下位に移動し、レジスタに格納されます。

メモリアクセス

メモリアクセスは、アルゴリズムを超えたパフォーマンスの最も重要な側面の1つです。 AAAゲームエンジンのようなパフォーマンスが重要なフィールドの多くは、より効率的なメモリアクセスパターンとレイアウトに要約されるデータ指向の最適化にエネルギーを集中させています。

ガベージコレクターを介して各ユーザー定義型を個別に割り当てようとする高水準言語の最大のパフォーマンス上の問題の1つは、たとえば、メモリをかなり断片化する可能性があることです。これは、すべてのオブジェクトが一度に割り当てられない場合に特に当てはまります。

このような場合、ユーザー定義オブジェクトタイプの100万インスタンスのリストを保存すると、それらのインスタンスへのループでの順次アクセスは、メモリの異なる領域を指す100万ポインタのリストに類似しているため、非常に遅くなる可能性があります。これらの場合、アーキテクチャは上位、低速、大きなレベルの階層のメモリをフェッチし、それらのチャンク内の周囲のデータが追い出される前にアクセスされることを期待して、大きな整列されたチャンクに入れます。そのようなリスト内の各オブジェクトが個別に割り当てられると、後続の各反復で、追い出しの前に隣接するオブジェクトにアクセスせずにメモリ内の完全に異なる領域からロードする必要がある場合に、キャッシュミスを支払うことになります。

このような言語の多くのコンパイラは、最近の命令選択とレジスタ割り当てで本当に素晴らしい仕事をしていますが、ここでのメモリ管理に対するより直接的な制御の欠如はキラーであり(多くの場合、エラーが発生しにくくなります)、さらにCとC++は非常に人気があります。

間接的にポインタアクセスを最適化

最もパフォーマンスが重要なシナリオでは、アプリケーションは多くの場合、隣接するチャンクからメモリをプールするメモリプールを使用して、参照の局所性を向上させます。そのような場合、そのノードのメモリレイアウトが本質的に隣接していれば、ツリーやリンクリストのようなリンクされた構造でもキャッシュフレンドリーにすることができます。これは、間接的に参照を逆参照するときに関連する参照の局所性を改善することによって間接的にではありますが、ポインタの逆参照をより効果的に安くしています。

追跡ポインタ

次のような単一リンクリストがあるとします。

Foo->Bar->Baz->null

問題は、これらすべてのノードを汎用アロケータに対して個別に割り当てると(一度にすべてではない可能性があります)、実際のメモリが次のように多少分散する可能性があることです(簡略図)。

enter image description here

ポインタの追跡を開始してFooノードにアクセスすると、次のように、強制ミス(およびおそらくページフォールト)によってチャンクがメモリ領域からメモリの低速領域から高速領域に移動します。そう:

enter image description here

これにより、メモリ領域をキャッシュ(おそらくページングも)して、その一部にのみアクセスし、残りを削除します。このリストの周りでポインタを追跡します。ただし、メモリアロケータを制御することで、このようなリストを次のように連続して割り当てることができます。

enter image description here

...これにより、これらのポインターを逆参照し、それらの指示先を処理する速度が大幅に向上します。したがって、非常に間接的ではありますが、この方法でポインタアクセスを高速化できます。もちろん、これらを配列に連続して格納した場合、最初はこの問題は発生しませんが、メモリレイアウトを明示的に制御できるメモリアロケータを使用すると、リンクされた構造が必要な日を節約できます。

*注:これは非常に単純化した図であり、メモリ階層と参照の局所性についての議論ですが、うまくいけば、質問のレベルに適しています。

3
user204677

これはメモリのオーバーヘッドではありませんか?

これは確かにメモリのオーバーヘッドですが、非常に小さいものです(重要ではありません)。

これはどのように補償されますか?

補償されません。ポインター(ポインターの逆参照)を介したデータアクセスが非常に高速であることを理解する必要があります(私が正しく覚えていれば、逆参照ごとに1つのAssembly命令しか使用しません)。それは多くの場合あなたが持っている最速の選択肢になるほど十分に速いです。

ポインタは、タイムクリティカルな低メモリアプリケーションで使用されますか?

はい。

1
utnapistim

必要なのは、追加のメモリ使用量(通常、ポインタあたり4〜8バイト)だけですが、そのポインタは必要です。これをより手頃な価格にする多くのテクニックがあります。

ポインターを強力にする最も基本的な手法は、すべてのポインターを保持する必要がないことです。アルゴリズムを使用して、ポインターから別のポインターへのポインターを構築できる場合があります。これの最も平凡な例は、配列演算です。 50の整数の配列を割り当てる場合、50のポインターを保持する必要はありません。各整数に1つです。通常、1つのポインター(最初のポインター)を追跡し、ポインター演算を使用して他のポインターをその場で生成します。場合によっては、配列の特定の要素へのこれらのポインタの1つを一時的に保持することがありますが、それは必要な間だけです。完了したら、後で再生成するのに十分な情報を保持している限り、必要に応じてそれを破棄できます。これはささいなことのように聞こえるかもしれませんが、メモリに保持しているポインタの数を本当に気にかけている場合に使用する種類の保存ツールとまったく同じです。

非常に厳しいメモリ状況では、これを使用してコストを最小限に抑えることができます。 veryタイトなメモリ空間で作業している場合は、通常、操作する必要のあるオブジェクトの数を把握できます。一度に1組の整数を割り当てて、それらへの完全なポインタを保持する代わりに、この特定のアルゴリズムでは256を超える整数は決して使用できないという開発者の知識を活用できます。その場合、最初の整数へのポインターを保持し、完全なポインター(4/8バイト)ではなく、char(1バイト)を使用してインデックスを追跡します。また、アルゴリズムトリックを使用して、これらのインデックスのいくつかをその場で生成することもできます。

この種の記憶の良心は過去に非常に人気がありました。たとえば、NESゲームは、大量のデータを格納するのではなく、データを詰め込んでアルゴリズムでポインタを生成する能力に大きく依存します。

極端なメモリ状況により、コンパイル時に操作するすべてのスペースを割り当てるなどの作業が行われる場合もあります。次に、そのメモリに格納する必要があるポインタは、データではなくプログラムに格納されます。多くのメモリに制約のある状況では、プログラムとデータのメモリが別々にある(多くの場合ROM vs RAM))ので、アルゴリズムを使用してポインタをそのプログラムメモリにプッシュする方法を調整できる場合があります。

基本的に、オーバーヘッドのすべてを取り除くことはできません。ただし、それを制御することはできます。アルゴリズム的手法を使用することにより、格納できるポインターの数を最小限に抑えることができます。動的メモリへのポインターをたまたま使用している場合、その動的メモリスポットへの1つのポインターを維持するコストを下回ることは決してありません。これは、そのメモリブロック内の何かにアクセスするために必要な最小限の情報だからです。ただし、超タイトなメモリ制約のシナリオでは、これは特殊なケースになる傾向があります(動的メモリと超タイトなメモリ制約は同じ状況では表示されない傾向があります)。

0
Cort Ammon

多くの場合、ポインタは実際にメモリを節約します。ポインタを使用する一般的な代替方法は、データ構造のコピーを作成することです。データ構造の完全なコピーは、ポインタより大きくなります。

タイムクリティカルなアプリケーションの1つの例は、ネットワークスタックです。適切なネットワークスタックは「ゼロコピー」になるように設計されます。これを行うには、ポインターを巧みに使用する必要があります。

0
paj28