このコードを検討してください:
_#include <utility>
#include <Tuple>
std::pair<int, int> f1()
{
return std::make_pair(0x111, 0x222);
}
std::Tuple<int, int> f2()
{
return std::make_Tuple(0x111, 0x222);
}
_
Clang 3と4は、x86-64で両方に対して同様のコードを生成します。
_f1():
movabs rax,0x22200000111
ret
f2():
movabs rax,0x11100000222 ; opposite packing order, not important
ret
_
ただし、Clang5はf2()
に対して異なるコードを生成します。
_f2():
movabs rax,0x11100000222
mov QWORD PTR [rdi],rax
mov rax,rdi
ret
_
GCC4からGCC7と同様に:
_f2():
movabs rdx,0x11100000222
mov rax,rdi
mov QWORD PTR [rdi],rdx ; GCC 4-6 use 2 DWORD stores
ret
_
単一のレジスタに収まる_std::Tuple
_を返すときに、生成されたコードが_std::pair
_と比べて悪いのはなぜですか? Clang 3と4が最適であるように見えたので、それは特に奇妙に思えますが、5はそうではありません。
ここで試してみてください: https://godbolt.org/g/T2Yqrj
簡単な答えは、Linuxでgcc
とclang
が使用するlibstc++
標準ライブラリの実装がstd::Tuple
を重要な移動コンストラクター(特に、_Tuple_impl
基本クラスには重要なmoveコンストラクターがあります)。一方、std::pair
のコピーおよび移動コンストラクターはすべてデフォルトです。
これにより、関数からこれらのオブジェクトを返したり、値で渡したりするための呼び出し規約にC++-ABI関連の違いが生じます。
SysV x86-64ABIに準拠するLinuxでテストを実行しました。このABIには、クラスまたは構造体を関数に渡したり返したりするための特定のルールがあります。これについては、 ここ について詳しく読むことができます。これらの構造体の2つのint
フィールドがINTEGER
クラスまたはMEMORY
クラスを取得するかどうかに関心のある特定のケース。
最近 バージョンのABI仕様には、次のように書かれています。
集合体(構造体と配列)と共用体タイプの分類は次のように機能します。
- オブジェクトのサイズが88バイトより大きい場合、または整列されていないフィールドが含まれている場合、クラスMEMORY12になります。
- C++オブジェクトに重要なコピーコンストラクタまたは重要なデストラクタのいずれかがある場合13、オブジェクトは非表示の参照によって渡されます(オブジェクトはパラメータリストでクラスINTEGERを持つポインタに置き換えられます)14。
- アグリゲートのサイズが単一の8バイトを超える場合、それぞれが個別に分類されます。各8バイトはクラスNO_CLASSに初期化されます。
- オブジェクトの各フィールドは再帰的に分類されるため、常に2つのフィールドが考慮されます。結果のクラスは、8バイトのフィールドのクラスに従って計算されます
ここで当てはまるのは条件(2)です。 moveコンストラクターではなく、コピーコンストラクターについてのみ言及していることに注意してください。ただし、一般的に必要なmoveコンストラクターの導入を考えると、おそらく仕様の欠陥にすぎないことは明らかです。以前にコピーコンストラクターが含まれていた分類アルゴリズムに含まれていること。特に、IA-64 cxx-abiは、gcc
が従うように文書化されています 移動コンストラクターを含みます :
パラメータタイプが呼び出しの目的で重要である場合、呼び出し元は一時用にスペースを割り当て、その一時を参照によって渡す必要があります。具体的には:
- スペースは、通常はスタック上で、通常の方法で一時的に呼び出し元によって割り当てられます。
そして 定義 自明ではない:
次の場合、タイプは呼び出しの目的で重要であると見なされます。
- 自明ではないコピーコンストラクタ、移動コンストラクタ、またはデストラクタ、または
- そのコピーおよび移動コンストラクターはすべて削除されます。
したがって、Tuple
はABIの観点から自明にコピー可能とは見なされないため、MEMORY
処理が適用されます。つまり、関数は呼び出されたrdi
によって渡されたスタック割り当てオブジェクト。 std::pair
関数は、1つのrax
に収まり、クラスEIGHTBYTE
を持っているため、INTEGER
の構造全体を返すことができます。
それは重要ですか?ええ、厳密に言えば、コンパイルしたようなスタンドアロン関数は、このABIの違いが「焼き付けられている」ため、Tuple
の効率が低下します。
ただし、多くの場合、コンパイラーは関数の本体を確認してインライン化したり、インライン化されていなくても手続き間分析を実行したりできます。どちらの場合も、ABIはもはや重要ではなく、少なくとも適切なオプティマイザーを使用すれば、両方のアプローチが同等に効率的である可能性があります。例 f1()
関数とf2()
関数を呼び出して、結果を計算してみましょう :
int add_pair() {
auto p = f1();
return p.first + p.second;
}
int add_Tuple() {
auto t = f2();
return std::get<0>(t) + std::get<1>(t);
}
原則として、add_Tuple
メソッドは、効率の低いf2()
を呼び出す必要があり、スタック上に一時的なタプルオブジェクトを作成して、非表示パラメーターとしてf2
に渡す必要があるため、不利な点から始まります。 。関係なく、両方の関数は完全に最適化されており、正しい値を直接返すだけです。
add_pair():
mov eax, 819
ret
add_Tuple():
mov eax, 819
ret
したがって、全体として、Tuple
に関するこのABIの問題の影響は比較的抑えられていると言えます。これにより、ABIに準拠する必要のある関数に小さな固定オーバーヘッドが追加されますが、これは相対的な意味でのみ実際に問題になります。非常に小さな関数ですが、そのような関数は、インライン化できる場所で宣言される可能性があります(そうでない場合は、パフォーマンスをテーブルに残します)。
上で説明したように、これはABIの問題であり、最適化の問題ではありません。 clangとgccはどちらも、ABIの制約の下で可能な限りライブラリコードをすでに最適化しています。std::Tuple
の場合にf1()
のようなコードを生成すると、ABI準拠の呼び出し元が破損します。
Linuxのデフォルトであるlibc++
ではなくlibstdc++
の使用に切り替えると、これを明確に確認できます。この実装には明示的な移動コンストラクターがありません(Marc Glisseがコメントで言及しているように、下位互換性のためにこの実装に固執しています)。ここで、clang
(そして、私は試していませんが、おそらくgcc)は、どちらの場合も 同じ最適なコード を生成します。
f1(): # @f1()
movabs rax, 2345052143889
ret
f2(): # @f2()
movabs rax, 2345052143889
ret
clang
のバージョンが異なる方法でコンパイルするのはなぜですか?それは単に clangのバグ またはあなたがそれをどのように見るかに応じて仕様のバグでした。仕様には、一時への非表示のポインターを渡す必要がある場合の移動構造が明示的に含まれていませんでした。 IA-64 C++ ABIに準拠していませんでした。たとえば、clangが使用していた方法でコンパイルすると、gcc
以降のバージョンのclang
と互換性がありませんでした。仕様は 最終的に更新されました およびclang バージョン5.0で動作が変更されました でした。
更新:Marc Glisse メンション 最初は重要な移動コンストラクターとC++の相互作用について混乱があったというコメントでABI、およびclang
は、ある時点で動作を変更しました。これは、おそらくスイッチを説明しています。
移動コンストラクターを含むいくつかの引数受け渡しのケースのABI仕様は不明確であり、それらが明確になると、clangはABIに従うように変更されました。これはおそらくそれらのケースの1つです。