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


29

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


11
動的メモリ割り当ての利点は、ポインタのコストを大きく上回ります。
ジェームズマクロード

32
他の言語(Java、C#、...)はオブジェクトへの参照をどのように保存すると思いますか?(ヒント:ポインターを使用します)。
エリックイド

6
ポインターはレジスターに置かれるか、引数として渡されます。どちらの場合も、明らかなメモリオーバーヘッドはありません。また、ポインターが計算される場合があります(例えば、ポインター演算、ポインターを返す関数など)
Basile Starynkevitch

9
(大きな)struct引数をアドレスで渡すのはどうですか?これをポインター変数として数えると、多くのアルゴリズムでは避けられず、構造体を値で渡すよりもはるかに少ないスペースを使用します!
PJTraill

4
これには、いくつかの宿題のような感じがあります。この質問は、回答がポインターを理解しているか、そしてそれらがどのように使用されているかを調査するように設計されています。
マイケルショー

回答:


34

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

これで、ポインターを使用するためにメモリの割り当て行う必要がなくなりました。int静的またはスタックで宣言された配列を持つことができ、インデックスの代わりにポインターを使用してints にアクセスできます。すべてとてもシンプルで効率的です。メモリの割り当ては不要であり、通常、ポインタは整数インデックスとまったく同じ量のメモリ領域を占有します。

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

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

また、C ++はオブジェクト指向言語であり、OOPには抽象化など、ポインターを使用してのみ達成可能な特定の側面があります。JavaやC#のような言語でさえ、ポインターを 広範囲に使用しますが、ポインターを直接操作することはできません。そうすることで、危険なことをするのを防ぐことができますが、それでも、これらの言語は、舞台裏ではすべてがポインターを使用して行われていることに気付きました。

だから、ポインタがされていないだけでタイムクリティカルな、低メモリの用途に使用される、彼らが使用されているどこでも


5
「ポインタが使用される主な理由は、動的なメモリ割り当てのためです」C ++ではそうかもしれませんが、Cでは必ずしもそうではありません。Cでは、ポインタが参照によって何かを渡す唯一の方法です。構造体全体をコピーしたくない場合は、その構造体へのポインタが必要になります。動的な割り当てをまったく行わない場合でも、それは依然として理にかなっています。たとえば、スタックにstruct foo f; init_foo(&f);割り当てfinit_fooから、その構造体へのポインタで呼び出します。それは非常に一般的です。(これらのポインターを「上方向」に渡さないように注意してください。)
ジョシュアテイラー

@JoshuaTaylorそれはとても正しいです、私はそれを忘れていました。私の答えに修正できますか?(コメントは短命ですが、答えはそうではありませんので、これはプログラマーSEの良い習慣と見なされます。)
マイクナキス

ぜひあなたの答えを追加してください。:)
ジョシュアテイラー

2
このブロックは、通常は数個のポインターと同じ大きさの(隠された)ヘッダーを持つため、これ自体でかなりのオーバーヘッドを表します。=>実際には、最新の実装でmallocは、割り当てられたブロックを「バケット」にクラスター化するため、ヘッダーオーバーヘッドが非常に低くなります。一方で、これは一般的には過剰割り当てに変換:あなたはこのように29を無駄に35のバイトを求めると、(あなたの知識がなくても)64を得る...
マシューM.

1
@MikeNakis:良く見えます、私に固執してくれてありがとう:)
Matthieu M.

35

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

確かに、追加のアドレス(一般的にはプロセッサに応じて4/8バイト)。

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

そうではない。ポインターに必要なインダイレクションが必要な場合は、支払いが必要です。

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

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


1
@DavidGrinbergこれは、戻り値の最適化がないことを前提としています
ダークホッグ

3
DOS用のTSRアプリケーションを作成するときに、80年代に15,000に収まらなければならなかったポインターを使用しました。はい、それらは低メモリアプリケーションで使用されます。
ロボット

1
@StevenBurnap TSRアプリで15kの低メモリを呼び出しますか?私はかつて16バイトのメモリしか消費しないTSRツールを書きました。
カスペルド

22
@kasperd:当時、ゼロしかなかった
ジャック


11

私はこれについて、Telastynとまったく同じスピンをしていません。

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

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

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

そのため、配列内を歩き回っている場合でも、目立たないローカル変数またはグローバル変数を取得している場合でも、ポインターの間接化に対して支払います。それは、それがどのような変数であるかに応じて、異なるポインターに基づいています。よくコンパイルされたコードは、使用するたびに再ロードするのではなく、CPUレジスタにそのポインターを保持します。


6

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

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

この考慮事項はすべてのプログラムに当てはまります。なぜなら、重複するコードを左右および中央に配置し、必要以上に20倍の恐ろしい、維持できない混乱を誰も構築したくないからです。


5

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

ポインタを保存する必要があると仮定します。それは常にそうではありません。すべての変数は、あるメモリアドレスに保存されます。longと宣言しているとしlong n = 5L;ます。これによりn、あるアドレスにストレージが割り当てられます。そのアドレスを使用して、の*((char *) &n) = (char) 0xFF;一部を操作するような凝ったことをすることができますn。のアドレスはn、追加のオーバーヘッドとしてどこにも保存されません。

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

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

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

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


いくつかの変数はメモリに保存されているが、唯一のレジスタにされていません
バジーレStarynkevitch

@BasileStarynkevitch:変数をレジスタに保持することを提案できますが、コンパイラはそうする必要はありません。アセンブラーでプログラミングしているのでなければ、実際の即時ストレージをそのレベルで制御することはできません。また、アセンブラーでも、呼び出すサブルーチンはレジスターをスタックに流し込み変数に使用できるようにます。したがって、どんな些細なプログラムでも、変数がメモリに少なくともある程度の時間を費やすことはほぼ保証されています。
TMN

コンパイラーは、一部のローカル変数のみをレジスターに保存することを許可される場合があり、ほとんどの最適化コンパイラーはそれを行いgcc -fverbose-asm -S -O2ます(Cコードをコンパイルしてみてください)
Basile Starynkevitch

@BasileStarynkevitch変数が純粋にレジスターに保存されるかもしれないという観察であなたがしようとしている点がわかりません。詳しく説明してもらえますか?
ローレンス

5

ポインターがあることは確かにいくらかのオーバーヘッドを消費しますが、逆さまにも見えます。ポインターはインデックスのようなものです。Cでは、ポインターのみによる文字列や構造などの複雑なデータ構造を使用できます。

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

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

ポインターを使用するのは、欠落しているデータ型のような他の選択肢がない場合にのみ使用する必要があります。ポインターを使用する構造は、適切に処理されないとデバッグが比較的難しくなり、エラーにつながる可能性があるためです


3

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

そうですね?

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

たとえば、ユーザーがボタンを押すだけで音楽ファイルがロードされ、ユーザーが別の音楽ファイルをロードしようとすると揮発性メモリからアンロードされる音楽プレーヤーを想像してください。

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

したがって、それは一種の「はい」であり、メモリアドレスを追跡するためにストレージが必要ですが、この種の動的に割り当てられたメモリに対してそれを回避できるわけではありません。

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

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

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

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

多くのパフォーマンス・クリティカルなアプリケーションは、ポインタの使用状況によって支配されているCまたはC ++で書かれているようはい、非常に一般的にそう、(彼らはスマートポインタなどの容器の後ろかもしれませんstd::vectorstd::string、しかし、根本的な仕組みは使用されているポインタに煮詰めます動的メモリブロックへのアドレスを追跡するため)。

この質問に戻りましょう:

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

ポインターは通常、100万個(64ビットマシンではまだわずか8メガバイト)を格納している場合を除き、非常に安価です。

* Benが指摘したように、「わずかな」8メガバイトは依然としてL3キャッシュのサイズであることに注意してください。ここでは、DRAMの総使用量と、ポインターの健全な使用が指すメモリチャンクに対する一般的な相対サイズという意味で、「わずか」に使用しました。

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

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

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

メモリアクセス

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

ガベージコレクターを介して各ユーザー定義型を個別に割り当てたい高レベル言語の最大のパフォーマンスの難しさの1つは、たとえば、かなりの量のメモリをフラグメント化できることです。これは、すべてのオブジェクトが一度に割り当てられるわけではない場合に特に当てはまります。

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

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

ポインターアクセスの間接的な最適化

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

ポインタを追いかける

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

Foo->Bar->Baz->null

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

*注:これは、参照のメモリ階層とローカリティに関する非常に単純化された図と説明ですが、問題のレベルに適していることを願っています。


1
「ちっぽけな8メガバイトは、」現代のCPUの全体のL3キャッシュの典型的なサイズである
ベンフォークト

1
@BenVoigt少し曖昧さをなくそうと思います。各ポインタは、メモリの32ビットのチャンク、例えば、を指した場合はもちろん、それは恐ろしいだろう

@BenVoigt脚注を追加して、その部分を明確にしようとしました!

その説明は良さそうです。
ベンフォークト

1

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

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

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

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

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

はい。


0

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

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

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

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

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

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


0

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

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

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