Cデータ構造に関する本を読んでいるときに、「メモリ効率の良い二重リンクリスト」という用語に出くわしました。メモリ効率の良い二重にリンクされたリストは、通常の二重にリンクされたリストよりも使用するメモリが少ないと言っているだけですが、同じ働きをします。これ以上の説明はなく、例も与えられていません。これはジャーナル、およびブラケットの「シンハ」から取られたことが与えられただけです。
グーグルで検索したところ、一番近いのは this でした。しかし、何も理解できませんでした。
誰かが私にCのメモリ効率の良い二重リンクリストとは何かを説明できますか?通常の二重リンクリストとどう違うのですか?
編集:わかりました、私は重大な間違いを犯しました。私が上に投稿したリンクを見てください、それは記事の2ページ目でした。最初のページがあることはわかりませんでした。指定されたリンクは最初のページだと思いました。記事の最初のページは実際に説明をしていますが、完璧だとは思いません。メモリ効率の高いリンクリストまたはXORリンクリスト)の基本概念についてのみ説明します。
これが私の2番目の答えであることはわかっていますが、ここで提供する説明は、最後の答えよりも良いと思います。しかし、その答えでも正しいことに注意してください。
メモリ効率の高いリンクリストは、XORリンクリストと呼ばれることが多いこれは[〜#〜] xor [〜#〜]ロジックゲートとそのプロパティに完全に依存しているためです。
はい、そうです。実際には、二重リンクリストとほぼ同じ仕事をしていますが、それは異なります。
二重リンクリストは、次のノードと前のノードを指す2つのポインタを格納しています。基本的に、戻りたい場合は、back
ポインターが指すアドレスに移動します。先に進む場合は、next
ポインターが指すアドレスに移動します。それは次のようなものです:
メモリ効率の高いリンクリスト、つまりXORリンクリストはポインターが2つではなく1つしかない。これは、前のアドレス(addr(prev))[〜#〜] xor [〜#〜](^)次のアドレス(addr(next ))。次のノードに移動する場合は、特定の計算を行い、次のノードのアドレスを見つけます。これは前のノードに行くのと同じです:
XORリンクリストは、その名前からわかるように、論理ゲートに大きく依存しています[〜 #〜] xor [〜#〜](^)とそのプロパティです。
プロパティは次のとおりです。
_|-------------|------------|------------|
| Name | Formula | Result |
|-------------|------------|------------|
| Commutative | A ^ B | B ^ A |
|-------------|------------|------------|
| Associative | A ^ (B ^ C)| (A ^ B) ^ C|
|-------------|------------|------------|
| None (1) | A ^ 0 | A |
|-------------|------------|------------|
| None (2) | A ^ A | 0 |
|-------------|------------|------------|
| None (3) | (A ^ B) ^ A| B |
|-------------|------------|------------|
_
これを脇に置いて、各ノードが何を格納するかを見てみましょう:
最初のノードまたはheadは、前のノードまたはアドレスがないため、0 ^ addr (next)
を格納します。それは次のようになります:
次に、2番目のノードはaddr (prev) ^ addr (next)
を格納します。それは次のようになります:
上の図は、ノードB、つまり2番目のノードを示しています。 AとCは、3番目と1番目のノードのアドレスです。すべてのノードを除く頭と尾は上記のものと同じです。
リストのtailには次のノードがないため、addr (prev) ^ 0
を格納します。それは次のようになります:
移動方法を確認する前に、XORリンクリストの表現をもう一度見てみましょう。
あなたが見るとき
それは明らかに、前後に移動する1つのリンクフィールドがあることを意味します。
また、XORリンクリストを使用する場合は、一時的な変数(ノードではない)が必要です。これには、以前にいたノードのアドレスが格納されます。次のノードに移動すると、古い値が破棄され、前のノードのアドレスが格納されます。
頭から次のノードへの移動
今、最初のノードまたはノードAにいるとします。ノードBに移動したいとします。これは、そのための式です。
_Address of Next Node = Address of Previous Node ^ pointer in the current Node
_
したがって、これは次のようになります。
_addr (next) = addr (prev) ^ (0 ^ addr (next))
_
これが先頭なので、以前のアドレスは単に0なので、次のようになります。
_addr (next) = 0 ^ (0 ^ addr (next))
_
括弧を削除できます:
_addr (next) = 0 ^ 0 addr (next)
_
none (2)
プロパティを使用すると、_0 ^ 0
_は常に0になると言えます。
_addr (next) = 0 ^ addr (next)
_
none (1)
プロパティを使用すると、次のように簡略化できます。
_addr (next) = addr (next)
_
次のノードのアドレスを取得しました!
ノードから次のノードへの移動
次に、前のノードと次のノードがある中間ノードにいるとします。
式を適用してみましょう:
_Address of Next Node = Address of Previous Node ^ pointer in the current Node
_
次に値を置き換えます。
_addr (next) = addr (prev) ^ (addr (prev) ^ addr (next))
_
括弧を削除:
_addr (next) = addr (prev) ^ addr (prev) ^ addr (next)
_
none (2)
プロパティを使用すると、次のことを簡略化できます。
_addr (next) = 0 ^ addr (next)
_
none (1)
プロパティを使用すると、次のことを簡略化できます。
_addr (next) = addr (next)
_
そして、あなたはそれを手に入れます!
ノードから以前のノードに移動する
タイトルを理解していない場合は、基本的にノードXにいて、ノードYに移動した場合、以前にアクセスしたノード、または基本的にノードXに戻りたいことを意味します。
これは面倒な作業ではありません。前述のとおり、一時変数に現在のアドレスを格納していることを思い出してください。したがって、訪問したいノードのアドレスは変数内にあります:
_addr (prev) = temp_addr
_
ノードから前のノードに移動する
これは、上記と同じではありません。つまり、ノードZにいて、ノードYにいて、ノードXに移動したいとします。
これは、ノードから次のノードに移動するのとほぼ同じです。これがまさにその逆です。プログラムを作成するときは、1つのノードから次のノードに移動するときに説明したのと同じ手順を使用します。次の要素を見つけるよりも、リスト内の前の要素を見つけるだけです。
これについて説明する必要はないと思います。
これは、二重リンクリストよりも少ないメモリを使用します。約33%少なくなります。
1つのポインターのみを使用します。これにより、ノードの構造が簡素化されます。
Doynaxが言ったように、XORのサブセクションは一定の時間で反転できます。
これは実装が少し難しいです。失敗する可能性が高く、デバッグは非常に困難です。
すべての変換(intの場合)は、_uintptr_t
_との間で行われる必要があります
ノードのアドレスを取得してそこからトラバース(または何でも)を開始することはできません。あなたは常に頭か尾から始めなければなりません。
ノードをジャンプしたりスキップしたりすることはできません。あなたは一人ずつ行かなければならない。
移動には、より多くの操作が必要です。
XORリンクリストを使用しているプログラムをデバッグするのは困難です。二重リンクリストをデバッグする方がはるかに簡単です。
これは、メモリを節約できる古いプログラミングトリックです。昔ほどメモリがタイトなリソースではなくなったので、もうあまり使われていないと思います。
基本的な考え方は次のとおりです。従来の二重リンクリストでは、隣接するリスト要素への2つのポインター、次の要素を指す「次の」ポインター、および前の要素を指す「前の」ポインターがあります。したがって、適切なポインタを使用して、リストを順方向または逆方向にトラバースできます。
メモリ削減の実装では、「next」と「prev」を「next」と「prev」のビット単位の排他的OR(ビット単位のXOR)である単一の値に置き換えます。したがって、隣接する要素ポインターのストレージを半分に減らします。
この手法を使用しても、リストをどちらの方向にトラバースすることも可能ですが、そのためには、前の(または次の)要素のアドレスを知る必要があります。たとえば、リストを順方向にトラバースしていて、「prev」のアドレスを持っている場合、「prev」のビットごとのXORを現在の結合ポインタ値と一緒に取ると、「次」を取得できます。 "prev" XOR "next"。結果は "prev" XOR "prev" XOR "next"、これは単に「次へ」です。反対方向でも同じことができます。
欠点は、結合されたポインターをデコードするコンテキストがないため、「prev」または「next」要素のアドレスを知らずに、その要素へのポインターを指定して要素を削除することはできないということです。値。
もう1つの欠点は、この種のポインタートリックが、コンパイラーが期待する通常のデータ型チェックメカニズムを回避することです。
これは巧妙なトリックですが、正直に言って、最近これを使用する理由はほとんどありません。
この質問の 2番目の回答 を確認することをお勧めします。しかし、私はこの答えが間違っているとは言っていません。これも正しいです。
メモリ効率の高いリンクリストは、 XORリンクリストとも呼ばれます。
[〜#〜] xor [〜#〜] (^)リンクリストは、next
およびback
ポインタを格納する代わりに、 one next
ポインターとback
ポインターの両方のジョブを実行するポインター。最初にXOR論理ゲートプロパティを見てみましょう。
|-------------|------------|------------|
| Name | Formula | Result |
|-------------|------------|------------|
| Commutative | A ^ B | B ^ A |
|-------------|------------|------------|
| Associative | A ^ (B ^ C)| (A ^ B) ^ C|
|-------------|------------|------------|
| None (1) | A ^ 0 | A |
|-------------|------------|------------|
| None (2) | A ^ A | 0 |
|-------------|------------|------------|
| None (3) | (A ^ B) ^ A| B |
|-------------|------------|------------|
例を見てみましょう。 A、B、C、D の4つのノードを持つ二重リンクリストがあります。以下にその外観を示します。
ご覧のとおり、各ノードには2つのポインタと、データを格納するための1つの変数があります。したがって、3つの変数を使用しています。
多数のノードを持つ二重リンクリストがある場合、使用するメモリが多すぎます。より効率的にするには、 Memory-Efficient Doubly Linked List を使用します。
Memory-Efficient Doubly Linked List は、XORとそのプロパティを使用してポインタを1つだけ前後に移動するリンクリストです。
これは絵による表現です:
どうやって前後に移動しますか?
一時変数があります(ノードにはありません)。ノードを左から右にトラバースしているとしましょう。したがって、ノードAから開始します。ノードAのポインタに、ノードBのアドレスを格納します。次に、ノードBに移動します。ノードBに移動するときに、一時変数にノードAのアドレスを格納します。
ノードBのリンク(ポインター)変数のアドレスはA ^ C
です。前のノードのアドレス(A)を取得し、XORを現在のリンクフィールドと組み合わせて、Cのアドレスを取得します。論理的には、次のようになります。
A ^ (A ^ C)
方程式を簡略化しましょう。次のようなAssociativeプロパティがあるため、括弧を削除して書き直すことができます。
A ^ A ^ C
これをさらに単純化して
0 ^ C
2番目の(表に記載されている「なし(2)」)プロパティがあるためです。
最初の(表に記載されている「なし(1)」)プロパティがあるため、これは基本的に
C
これらすべてを理解できない場合は、3番目のプロパティ(表に記載されている「なし(3)」)を参照してください。
これで、ノードCのアドレスを取得しました。これは、戻るプロセスと同じです。
ノードCからノードBに移動するとします。ノードCのアドレスを一時変数に格納し、上記のプロセスを再度実行します。
注:A
、B
、C
などはすべてアドレスです。明確にするように言ってくれたバトシェバに感謝します。
XORリンクリストの欠点
Lundinが述べたように、すべての変換はuintptr_t
との間で行われる必要があります。
Sami Kuhmonenが述べたように、ランダムノードだけでなく、明確に定義された開始点から開始する必要があります。
ノードをジャンプすることはできません。あなたは順番に行かなければならない。
また、XORリンクリストは、ほとんどの場合、二重リンクリストよりもではありません。
参照
わかりましたので、XORリンクリストを参照してください。これにより、項目ごとに1つのポインターが節約されます...しかし、これは醜い、醜いデータ構造であり、実行できる最善の方法とはかけ離れています。
メモリが心配な場合は、リンクされた配列のリストのように、ノードごとに複数の要素を持つ二重にリンクされたリストを使用することをお勧めします。
たとえば、XORリンクリストは、アイテムごとに1ポインターとアイテム自体を要しますが、ノードごとに16アイテムを持つ二重リンクリストは、16アイテムごとに3ポインター、または3/16アイテムごとのポインター(追加のポインターは、ノードにあるアイテムの数を記録する整数のコストです)1未満です。
メモリの節約に加えて、ノード内のすべての16のアイテムがメモリ内で互いに隣接しているため、局所性に利点があります。リストを反復するアルゴリズムはより高速になります。
XORリンクリストでは、ノードを追加または削除するたびにメモリを割り当てるか解放する必要がありますが、これは負荷の高い操作です。配列にリンクされたリストを使用すると、ノードを完全に満たすことができなくなるため、これよりも優れた処理を行うことができます。たとえば、5つの空のアイテムスロットを許可すると、最低3回の挿入または削除ごとにメモリの割り当てまたは解放のみが行われます。
ノードを割り当てたり解放したりする方法とタイミングを決定するための多くの可能な戦略があります。
XORリンクされたリストのかなり精巧な説明はすでに得られています。メモリの最適化に関するさらにいくつかのアイデアを共有します。
ポインターは通常、64ビットマシンでは8バイトを使用します。 RAMの任意のポイントを32ビットポインタでアドレス可能な4GBを超えてアドレスする必要があります。
メモリマネージャは通常、バイトではなく固定サイズのブロックを扱います。例えば。 C mallocは通常、16バイトの粒度で割り当てます。
これらの2つのことは、データが1バイトの場合、対応する二重にリンクされたリスト要素は32バイト(8 + 8 + 1、最も近い16の倍数に切り上げ)になることを意味します。 XORトリックを使用すると、16に減らすことができます。
ただし、さらに最適化するには、独自のメモリマネージャーを使用することを検討できます。これは、(a)1バイトなどの粒度の低いブロックを処理するか、場合によってはビットに移行することもあり、(b)全体のサイズに厳しい制限があります。たとえば、リストが常に100 MBの連続ブロック内に収まることがわかっている場合、そのブロック内の任意のバイトをアドレス指定するために必要なのは27ビットだけです。 32ビットまたは64ビットではありません。
ジェネリックリストクラスを開発していないが、アプリケーションの特定の使用パターンを知っている場合、多くの場合、このようなメモリマネージャーの実装は簡単な作業です。たとえば、1000を超える要素を割り当てることはなく、各要素が5バイトを使用することがわかっている場合は、最初の空きバイトのインデックスを保持する変数を持つ5000バイトの配列としてメモリマネージャーを実装し、追加の要素を割り当てるときに、そのインデックスを取得し、割り当てられたサイズだけ前方に移動します。この場合、ポインターは実際のポインター(int *など)ではなく、その5000バイト配列内の単なるインデックスになります。