web-dev-qa-db-ja.com

T *をレジスタに渡すことができるのに、unique_ptr <T>はできないのはなぜですか?

私はCppCon 2019でチャンドラー・カルースの講演を見ています:

ゼロコストの抽象化はありません

その中で、彼は、_std::unique_ptr<int>_よりも_int*_を使用した場合に生じるオーバーヘッドの大きさに驚いた様子の例を示しています。そのセグメントは、およそ17:25の時点で始まります。

あなたは彼のサンプルのペアのスニペット(godbolt.org)の コンパイル結果 を見ることができます-実際に、コンパイラがunique_ptr値を渡そうとしないようです-これは実際のところ、一番下の行は単なるアドレスです-レジスターの中だけで、まっすぐなメモリの中でのみ。

Carruth氏が27:00頃に指摘する点の1つは、C++ ABIが値渡しパラメーター(すべてではないが、一部ではない可能性があります。レジスター内ではなく。

私の質問:

  1. これは実際には一部のプラットフォームでのABI要件ですか? (どれですか?)または、特定のシナリオでの悲観化に過ぎないのでしょうか?
  2. なぜABIはそのようなのですか?つまり、構造体/クラスのフィールドがレジスター内、または単一のレジスター内に収まる場合、なぜそのレジスター内で渡すことができないのでしょうか?
  3. C++標準委員会は、この点について近年、またはこれまでに議論しましたか?

PS-この質問をコードなしで残さないように:

単純なポインタ:

_void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}
_

一意のポインタ:

_using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}
_
84
einpoklum
  1. これは実際にはABI要件ですか、それとも特定のシナリオにおける単なる悲観化ですか?

一例は System V Application Binary Interface AMD64 Architecture Processor Supplement です。このABIは、64ビットx86互換CPU(Linux x86_64アーキテクチャー)用です。 Solaris、Linux、FreeBSD、macOS、LinuxのWindowsサブシステムで使用されています。

C++オブジェクトに重要なコピーコンストラクターまたは重要なデストラクタがある場合、それは非表示の参照によって渡されます(オブジェクトは、クラスINTEGERを持つポインターによってパラメーターリストで置き換えられます)。

重要なコピーコンストラクタまたは重要なデストラクタのいずれかを持つオブジェクトは、明確に定義されたアドレスを持つ必要があるため、値で渡すことはできません。関数からオブジェクトを返すときにも同様の問題が発生します。

自明なコピーコンストラクターと自明なデストラクタで1つのオブジェクトを渡すために使用できる汎用レジスタは2つだけです。つまり、sizeofが16以下のオブジェクトの値のみがレジスタで渡されることに注意してください。呼び出し規約の詳細な扱い、特に§7.1オブジェクトの受け渡しと返却については、 Agner Fogによる呼び出し規約 を参照してください。レジスターでSIMDタイプを渡すための別個の呼び出し規則があります。

他のCPUアーキテクチャには異なるABIがあります。


  1. なぜABIはそのようなのですか?つまり、構造体/クラスのフィールドがレジスター内、または単一のレジスター内に収まる場合、なぜそのレジスター内で渡すことができないのでしょうか?

これは実装の詳細ですが、例外が処理されると、スタックの巻き戻し中に、自動ストレージ期間が破棄されるオブジェクトは、その時点までにレジスターが破棄されているため、関数スタックフレームに対して相対的にアドレス可能でなければなりません。スタック巻き戻しコードは、デストラクタを呼び出すためにオブジェクトのアドレスを必要としますが、レジスタ内のオブジェクトにはアドレスがありません。

Pedantically、 デストラクタはオブジェクトを操作します

オブジェクトは、その構築期間([class.cdtor])、存続期間、および破棄期間のストレージ領域を占有します。

オブジェクトのIDはそのアドレス であるため、addressableストレージがオブジェクトに割り当てられていない場合、オブジェクトはC++に存在できません。

単純なコピーコンストラクターがレジスターに保持されているオブジェクトのアドレスが必要な場合、コンパイラーはオブジェクトをメモリに格納してアドレスを取得するだけです。一方、コピーコンストラクターが重要な場合、コンパイラーはそれをメモリに格納するだけではなく、参照を取得するコピーコンストラクターを呼び出す必要があるため、レジスターにオブジェクトのアドレスを必要とします。呼び出し規約は、おそらくコピーコンストラクターが呼び出し先でインライン化されたかどうかに依存できません。

これについて考えるもう1つの方法は、自明にコピー可能な型の場合、コンパイラはオブジェクトのvalueをレジスタに転送し、そこからオブジェクトを回復できるということです。必要に応じて、単純なメモリストア。例えば。:

void f(long*);
void g(long a) { f(&a); }

system Vを使用するx86_64では、ABIは次のようにコンパイルされます。

g(long):                             // Argument a is in rdi.
        Push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

彼の示唆に富むトークChandler Carruth mentions では、物事を改善する可能性のある破壊的な動きを実装するために、(特に)破壊的なABIの変更が必要になる場合があります。 IMO、新しいABIを使用する関数が明示的にオプトインして新しい別のリンケージを持つ場合、ABIの変更は非破壊的である可能性があります。それらをextern "C++20" {}ブロックで宣言します(おそらく、既存のAPIを移行するための新しいインライン名前空間で)。そのため、新しいリンケージを持つ新しい関数宣言に対してコンパイルされたコードのみが新しいABIを使用できます。

呼び出された関数がインライン化されている場合、ABIは適用されないことに注意してください。リンク時のコード生成と同様に、コンパイラは他の翻訳単位で定義された関数をインライン化したり、カスタムの呼び出し規約を使用したりできます。

47

一般的なABIでは、重要なデストラクタ->レジスタを渡すことができません

(コメント内の@haroldの例を使用した@MaximEgorushkinの回答のポイントのイラスト。@ Yakkのコメントに従って修正されました。)

コンパイルした場合:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

あなたが得る:

test(Foo):
        mov     eax, edi
        ret

つまり、Fooオブジェクトは、レジスタ(test)でediに渡され、レジスタ(eax)でも返されます。

デストラクタが自明ではない場合(OPのstd::unique_ptrの例のように)-一般的なABIはスタックに配置する必要があります。これは、デストラクタがオブジェクトのアドレスをまったく使用しない場合でも当てはまります。

したがって、何もしないデストラクタの極端な場合でも、コンパイルすると:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

あなたが得る:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

無駄なローディングと保存で。

8
einpoklum

これは実際には一部のプラットフォームでのABI要件ですか? (どれですか?)または、特定のシナリオでの悲観化に過ぎないのでしょうか?

コンパイルユニットの境界で何かが表示されている場合、それが暗黙的に定義されていても明示的に定義されていても、ABIの一部になります。

なぜABIはそのようなのですか?

基本的な問題は、コールスタックを上下に移動すると、レジスタが常に保存および復元されることです。そのため、それらへの参照またはポインタを持つことは現実的ではありません。

インライン化とそれから生じる最適化は、それが発生したときに素晴らしいですが、ABI設計者はそれが発生することに依存できません。最悪のケースを想定してABIを設計する必要があります。プログラマーが、最適化レベルに応じてABIが変更されたコンパイラーに満足することはないと思います。

論理コピー操作は2つの部分に分割できるため、簡単にコピーできる型をレジスタに渡すことができます。パラメーターは、呼び出し元がパラメーターを渡すために使用されるレジスターにコピーされ、呼び出し先がローカル変数にコピーします。したがって、ローカル変数がメモリ位置を持っているかどうかは、呼び出し先の問題のみです。

一方、コピーまたは移動コンストラクターを使用する必要がある型では、コピー操作をこのように分割できないため、メモリに渡す必要があります。

C++標準委員会は、この点について近年、またはこれまでに議論しましたか?

標準化団体がこれを検討したかどうかはわかりません。

私への明白な解決策は、適切な破壊的な動き(「有効だがそれ以外は指定されていない状態」の現在の中途半端な家ではなく)を言語に追加し、タイプに「些細な破壊的な動き」を許可するフラグを付ける方法を導入することです「それは些細なコピーを許可していなくても。

ただし、このようなソリューションでは、既存のコードを実装するために既存のコードのABIを破壊する必要があるため、かなりの抵抗が生じる可能性があります(ただし、新しいC++標準バージョンの結果としてのABIの破壊は前例のないものではなく、たとえばstd :: stringの変更など) C++ 11では、ABIブレークが発生しました。

2
plugwash