64ビットポインターの余分な16ビットを使用する 質問する

64ビットポインターの余分な16ビットを使用する 質問する

私はそれを読んだ64ビットマシンは実際には48ビットのアドレスしか使用しない(具体的には、Intel Core i7 を使用しています)。

余分な 16 ビット (ビット 48 ~ 63) はアドレスには関係がないので無視されると思います。しかし、そのようなアドレスにアクセスしようとすると、信号が届きましたEXC_BAD_ACCESS

私のコードは次のとおりです:

int *p1 = &val;
int *p2 = (int *)((long)p1 | 1ll<<48);//set bit 48, which should be irrelevant
int v = *p2; //Here I receive a signal EXC_BAD_ACCESS.

なぜそうなるのでしょうか? これらの 16 ビットを使用する方法はあるのでしょうか?

これを使用すると、よりキャッシュフレンドリーなリンク リストを構築できます。次の ptr に 8 バイト、キーに 8 バイト (アラインメント制限のため) を使用する代わりに、キーをポインターに埋め込むことができます。

ベストアンサー1

上位ビットは、将来アドレスバスが増加する場合に備えて予約されているため、単純にそのまま使用することはできません。

AMD64アーキテクチャは64ビットの仮想アドレス形式を定義しており、その下位48ビットが現在の実装で使用されています(...)アーキテクチャ定義将来の実装ではこの制限を64ビットまで引き上げることができる。仮想アドレス空間を 16 EB (2 64バイト) に拡張します。これは、x86 の4 GB (2 32バイト)と比較されます。

http://en.wikipedia.org/wiki/X86-64#アーキテクチャ機能

さらに重要なのは、同じ記事によると次の通りである(強調は筆者による)。

...アーキテクチャの最初の実装では、仮想アドレスの最下位48ビットのみが実際にアドレス変換(ページテーブル検索)に使用されていました。さらに、仮想アドレスのビット48から63はビット47のコピーでなければならない(似たような方法でサイン拡張) に従わない場合、プロセッサは例外を発生させます。この規則に準拠するアドレスは、「標準形式」と呼ばれます。

CPU は、たとえ使用されていない場合でも上位ビットをチェックするので、実際には「無関係」というわけではありません。ポインタを使用する前に、アドレスが正規であることを確認する必要があります。

最近の CPU の中には、上位ビットを無視して、最上位ビットがビット #47 (PML4) または #56 (PML5) と一致するかどうかのみをチェックするものもあります。Intelリニア アドレス マスキング (LAM)カーネルは、ユーザー空間、カーネル、またはその両方に対して、プロセスごとに有効にすることができます。AMDUAI (上位アドレス無視)同様です。ARM64にも同様の機能があり、トップバイト無視 (TBI)これにより、ポインターにデータを格納するのがより効率的かつ簡単になります (参照解除する前に手動でデータを削除する必要も、タグ付けを認識しない関数に渡す必要もありません)。


とはいえ、x86_64では必要に応じて上位16ビットを自由に使用可能(仮想アドレスが48ビットより広くない場合は、以下を参照)ただし、ポインタ値をチェックして修正する必要があります。符号拡張逆参照する前にそれを実行します。

ポインタ値をキャストすることlong正しいやり方ではないlongポインタを格納するのに十分な幅があるかどうかは保証されていないため、uintptr_tまたはintptr_t

int *p1 = &val; // original pointer
uint8_t data = ...;
const uintptr_t MASK = ~(1ULL << 48);

// === Store data into the pointer ===
// Note: To be on the safe side and future-proof (because future implementations
//     can increase the number of significant bits in the pointer), we should
//     store values from the most significant bits down to the lower ones
int *p2 = (int *)(((uintptr_t)p1 & MASK) | (data << 56));

// === Get the data stored in the pointer ===
data = (uintptr_t)p2 >> 56;

// === Deference the pointer ===
// Sign extend first to make the pointer canonical
// Note: Technically this is implementation defined. You may want a more
//     standard-compliant way to sign-extend the value
intptr_t p3 = ((intptr_t)p2 << 16) >> 16;
val = *(int*)p3;

WebKit の JavaScriptCore と Mozilla の SpiderMonkey エンジン同様にルアJITこれをナンボクシングテクニック値がNaNの場合、下位48ビットにはオブジェクトへのポインタ上位 16 ビットはタグ ビットとして機能し、それ以外の場合は double 値になります。

以前LinuxはGSベースアドレスの63番目のビットも使用します値がカーネルによって書き込まれたかどうかを示す

実際には、通常48番目のビットも使用できます。最近の64ビットOSのほとんどはカーネルとユーザースペースを半分に分割しているため、ビット47は常にゼロで、上位17ビットが自由に使用できます。


また、下位ビットデータを保存するためのものです。タグ付きポインタが 4 バイトに揃えられている場合int、下位 2 ビットは常に 0 となり、32 ビット アーキテクチャと同様に使用できます。64 ビット値の場合、下位 3 ビットは既に 8 バイトに揃えられているため、これらを使用できます。この場合も、逆参照する前にこれらのビットをクリアする必要があります。

int *p1 = &val; // the pointer we want to store the value into
int tag = 1;
const uintptr_t MASK = ~0x03ULL;

// === Store the tag ===
int *p2 = (int *)(((uintptr_t)p1 & MASK) | tag);

// === Get the tag ===
tag = (uintptr_t)p2 & 0x03;

// === Get the referenced data ===
// Clear the 2 tag bits before using the pointer
intptr_t p3 = (uintptr_t)p2 & MASK;
val = *(int*)p3;

このエンジンの有名な使用者は、V8エンジンです。SMI (小整数) 最適化アドレスの最下位ビットは、次のタイプのタグとして機能します。

  • 1の場合値は実データ(オブジェクト、浮動小数点数、またはより大きな整数)へのポインタです。次の上位ビット(w)は、ポインタが弱いか強いかを示します。タグビットをクリアして逆参照するだけです。
  • 0の場合、これは小さな整数です。32ビットV8またはポインタ圧縮付きの64ビットV8では31ビット整数なので、1の符号付き右シフトを実行して値を復元します。ポインタ圧縮なしの64ビットV8では、上位半分は32ビット整数です。
   32-bit V8
                           |----- 32 bits -----|
   Pointer:                |_____address_____w1|
   Smi:                    |___int31_value____0|
   
   64-bit V8
               |----- 32 bits -----|----- 32 bits -----|
   Pointer:    |________________address______________w1|
   Smi:        |____int32_value____|0000000000000000000|

https://v8.dev/blog/ポインター圧縮


下記のコメントにあるように、インテルはPML5これは57ビット仮想アドレス空間このようなシステムでは、上位7ビットしか使用できません。

それでも、空きビットを増やすためにいくつかの回避策を講じることができます。まず、64 ビット OS で 32 ビット ポインターを使用してみます。Linux では、x32abi が許可されている場合、ポインターは 32 ビット長になります。Windows では、フラグをクリアするだけで/LARGEADDRESSAWARE、ポインターは 32 ビット長になり、上位 32 ビットを目的に使用できます。Windows で X32 を検出するにはどうすればいいですか?もう一つの方法は、ポインタ圧縮トリック:V8 の圧縮ポインター実装は、JVM の圧縮 Oops とどう違うのでしょうか?

OS にメモリを下位領域のみに割り当てるように要求することで、さらにビット数を増やすことができます。たとえば、アプリケーションが 64 MB を超えるメモリを使用しないようにできる場合は、26 ビットのアドレスのみが必要です。また、すべての割り当てが 32 バイトで整列されている場合は、さらに 5 ビット使用できるため、ポインターに 64 - 21 = 43 ビットの情報を格納できます。

私は推測するゼグその一例です。アドレス指定には42ビットしか使用しないため、2 42バイト = 4 × 2 40バイト = 4 TBの容量が可能です。

したがって、ZGC は 4TB のアドレスから始まる 16TB のアドレス空間を予約します (ただし、実際にこのメモリのすべてを使用するわけではありません)。

ZGC の初見

ポインター内のビットは次のように使用されます。

 6                 4 4 4  4 4                                             0
 3                 7 6 5  2 1                                             0
+-------------------+-+----+-----------------------------------------------+
|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
+-------------------+-+----+-----------------------------------------------+
|                   | |    |
|                   | |    * 41-0 Object Offset (42-bits, 4TB address space)
|                   | |
|                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0
|                   |                                 0010 = Marked1
|                   |                                 0100 = Remapped
|                   |                                 1000 = Finalizable
|                   |
|                   * 46-46 Unused (1-bit, always zero)
|
* 63-47 Fixed (17-bits, always zero)

詳しい方法については、


サイドノート:ポインタに比べてキー値が小さいケースでリンクリストを使用すると、メモリの無駄が多くなり、キャッシュの局所性が悪いために速度も遅くなります。実際、ほとんどの実際の問題ではリンクリストを使用すべきではありません。

おすすめ記事