1. ホーム
  2. c++

[解決済み] T*はレジスタに渡せるのに、unique_ptr<T>は渡せないのはなぜですか?

2023-02-05 10:35:39

質問

CppCon 2019でChandler Carruthのトークを見ています。

ゼロコストの抽象化は存在しない

この中で、彼は std::unique_ptr<int> の上に int* このセグメントは17:25に始まります。

を見ることができます。 コンパイル結果 を見てください。コンパイラは 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));
}

どのように解決するのですか?

<ブロッククオート
  1. これは実際に ABI の要件なのでしょうか、それとも特定のシナリオで悲観的になっているだけなのでしょうか。
ブロック クォート

1 つの例として System V Application Binary Interface AMD64 Architecture Processor Supplement (英語) . これは64ビットx86互換CPU(Linux x86_64 architecure)用のABIです。Solaris, Linux, FreeBSD, macOS, Windows Subsystem for Linuxで使用可能です。

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

非自明なコピーコンストラクタまたは非自明なデストラクタを持つオブジェクトは、値で渡すことができません。 なぜなら、そのようなオブジェクトは明確に定義されたアドレスを持っていなければならないからです。同様の問題は 関数からオブジェクトを返す場合にも同様の問題があります。

些細なコピーコンストラクタと些細なデストラクタを持つ1つのオブジェクトを渡すために、2つの汎用レジスタしか使用できないことに注意してください。 sizeof を持つオブジェクトの値のみがレジスタに渡されます。参照 呼び出しの規約 by Agner Fog 特に§7.1 オブジェクトの受け渡しと返却を参照してください。レジスタの SIMD 型を渡すための別の呼び出し規約があります。

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


また Itanium C++ ABI もあり、これはほとんどのコンパイラが準拠しています (MSVC は別として) が、これは が必要とする :

パラメータの型が呼び出しの目的のために自明でない場合、呼び出し側はテンポラリのためのスペースを確保し、そのテンポラリを参照渡ししなければなりません。

以下の場合、型は呼び出しの目的のために非自明であるとみなされます。

  • 非自明なコピーコンストラクタ、ムーブコンストラクタ、デストラクタを持つか、または
  • コピーとムーブのコンストラクタがすべて削除される。

この定義は、クラス型に適用され、型を渡したり返したりするときに余分な一時が許される型の [class.temporary]p3 における定義を補完することを意図しています。ABI の目的のために些細な型は、レジスタなど、ベース C ABI の規則に従って渡され、返されます。


<ブロッククオート
  1. なぜ ABI はこのような仕様になっているのでしょうか。つまり、構造体/クラスのフィールドがレジスタ内に、あるいは単一のレジスタに収まるのであれば、なぜそのレジスタ内で渡すことができないのでしょうか?

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

衒学的に デストラクタはオブジェクトに対して動作します。 :

オブジェクトは、構築時([class.cdtor])、寿命時、破棄時にストレージの領域を占有します。

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

レジスタに保持された些細なコピーコンストラクタを持つオブジェクトのアドレスが必要な場合、コンパイラは単にオブジェクトをメモリに格納してアドレスを取得することができます。一方、コピーコンストラクタが自明でない場合、コンパイラは単にメモリに格納することはできず、むしろ参照を取るコピーコンストラクタを呼び出す必要があり、したがって、レジスタ内のオブジェクトのアドレスが必要になります。呼び出しの規約はおそらく、コピー コンストラクタが呼び出し側でインライン化されているかどうかに依存することはできません。

このことについて考えるもう一つの方法は、自明なコピー可能型に対して、コンパイラが をレジスタに転送し、そこから必要であればプレーンメモリストアによってオブジェクトを復元することができる、ということです。例えば

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

を x86_64 で System V 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.


チャンドラー・カルース氏の示唆に富んだ講演の中で 言及 で、物事を改善できる破壊的な動きを実装するために、(とりわけ) 破壊的な ABI 変更が必要かもしれないと述べています。IMO では、新しい ABI を使用する関数が新しい別のリンクを持つことを明示的に選択した場合、ABI の変更は壊れることはありません。 extern "C++20" {} ブロック (おそらく、既存の API を移行するための新しいインライン名前空間) で宣言します。これにより、新しい関数宣言に対して新しいリンケージでコンパイルされたコードのみが新しい ABI を使用できるようになります。

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