33

今日まで、構造体が十分に大きくて後者の方が高速である場合、適切なコンパイラは構造体の値渡しを参照渡しに自動的に変換するといつも思っていました。私の知る限り、これは非常に簡単な最適化のようです。しかし、これが実際に起こるかどうかという好奇心を満たすために、C++ とDの両方で簡単なテスト ケースを作成し、GCC と Digital Mars D の両方の出力を調べました。問題の関数は、渡された構造体を変更せずに、メンバーを追加して値を返しました。C++ バージョンは以下のとおりです。

#include "iostream.h"

struct S {
    int i, j, k, l, m, n, o, p;
};

int foo(S s) {
    return s.i + s.j + s.k + s.l + s.m + s.n + s.o + s.p;
}

int main() {
    S s;
    int bar = foo(s);
    cout << bar;
}

int私の質問は、これらすべてのs を実際にスタックにプッシュするのではなく、コンパイラがこのようなものを参照渡しに最適化しないのはなぜですか?

注: 使用されるコンパイラ スイッチ: GCC -O2 (-O3 inlined foo().)、DMD -O -inline -release。

編集:明らかに、一般的なケースでは、値渡しと参照渡しのセマンティクスは同じではありません。たとえば、コピー コンストラクターが含まれている場合や、呼び出し先で元の構造体が変更されている場合などです。ただし、多くの現実世界のシナリオでは、セマンティクスは観察可能な動作に関して同一です。これらは私が尋ねているケースです。

4

12 に答える 12

30

C/C++ では、コンパイラは関数宣言のみに基づいて関数呼び出しをコンパイルできる必要があることを忘れないでください。

呼び出し元がその情報のみを使用している可能性があることを考えると、コンパイラが関数をコンパイルして、話している最適化を利用する方法はありません。呼び出し元は、関数が何も変更しないことを知ることができないため、ref で渡すことができません。詳細な情報が不足しているために一部の呼び出し元が値渡しを行う可能性があるため、値渡しを想定して関数をコンパイルする必要があり、すべての人が値渡しを行う必要があります。

パラメーターを ' ' としてマークした場合でもconst、コンパイラーは最適化を実行できないことに注意してください。これは、関数が嘘をつき、constness をキャストする可能性があるためです (これは、渡されるオブジェクトが実際には const ではありません)。

関数には外部リンケージがないため、静的関数(または匿名の名前空間にある関数)の場合、コンパイラはあなたが話している最適化を行う可能性があると思います。関数のアドレスが他のルーチンに渡されたり、ポインターに格納されたりしない限り、他のコードから呼び出すことはできません。この場合、コンパイラはすべての呼び出し元を完全に認識できるため、最適化を行うことができると思います。

適用できるものがあるかどうかはわかりません (実際、適用できるものがあるとしたら驚くでしょう。あまり頻繁には適用できない可能性があるからです)。

もちろん、プログラマーとして (C++ を使用している場合)、const&可能な限りパラメーターを使用してコンパイラーにこの最適化を実行させることができます。コンパイラが自動的にそれを実行できない理由を尋ねていることは知っていますが、これが次善の策だと思います。

于 2009-02-16T03:29:26.577 に答える
13

問題は、ユーザー コードの意図についてコンパイラに判断を求めることです。コピー コンストラクターで何かを実行できるように、超大規模な構造体を値渡ししたいのかもしれません。私を信じてください、そこにいる誰かが、まさにそのようなシナリオのためにコピーコンストラクターで有効に呼び出される必要があるものを持っています。by ref に切り替えると、コピー コンストラクターがバイパスされます。

これをコンパイラが生成した決定にするのは悪い考えです。その理由は、コードの流れについて推論することが不可能になるからです。通話を見て、それが正確に何をするかを知ることはできません。a) コードを理解し、b) コンパイラーの最適化を推測する必要があります。

于 2009-02-16T03:21:29.370 に答える
10

1 つの答えは、呼び出されたメソッドが構造体の内容を変更していないことをコンパイラが検出する必要があるというものです。その場合、参照渡しの効果は値渡しの効果とは異なります。

于 2009-02-16T03:19:30.267 に答える
4

一部の言語のコンパイラは、呼び出される関数にアクセスでき、呼び出される関数が変更されないと想定できる場合、これを行うことができます。これはグローバル最適化と呼ばれることもあり、一部の C または C++ コンパイラは実際にこのようなケースを最適化する可能性が高いようです。このような単純な関数のコードをインライン化することにより、より可能性が高くなります。

于 2009-02-16T03:22:28.807 に答える
4

これは間違いなく実装できる最適化だと思います (いくつかの仮定の下で、最後の段落を参照してください)。引数をスタックにプッシュする (または呼び出し規約に応じてレジスタを介して渡す) 代わりに、値を読み取るポインターをプッシュします。この余分な間接化にはサイクルが必要です。また、渡された引数がレジスタではなくメモリ内にある必要があります (そのため、それを指すことができます)。渡されるレコードに多くのフィールドがあり、レコードを受け取る関数がそれらの一部のみを読み取る場合にのみ有益です。インダイレクションによって浪費される余分なサイクルは、不要なフィールドをプッシュすることによって無駄にされないサイクルを補う必要があります。

逆の最適化である引数の昇格が実際に LLVM に実装されていることに驚くかもしれません。これは、読み取り専用の少数のフィールドを持つ内部関数の参照引数を値引数に (または集計をスカラーに) 変換します。これは、ほとんどすべてを参照渡しする言語で特に役立ちます。これにデッド引数の削除を続けると、触れられていないフィールドを渡す必要もありません。

関数の呼び出し方法を変更する最適化は、最適化される関数がコンパイルされるモジュールの内部にある場合にのみ機能することに注意してください (これはstatic、C で関数を宣言し、C++ でテンプレートを使用することによって得られます)。オプティマイザーは、関数だけでなくすべての呼び出しポイントも修正する必要があります。これにより、リンク時に実行しない限り、そのような最適化の範囲がかなり制限されます。さらに、コピー コンストラクターが関与している場合 (他の投稿者が言及しているように)、最適化はプログラムのセマンティクスを変更する可能性があるため、呼び出されることはありません。

于 2009-02-16T04:17:58.893 に答える
2

値渡しから参照渡しに変更すると、関数のシグネチャが変更されます。関数が静的でない場合、実行した最適化を認識していない他のコンパイル ユニットのリンク エラーが発生します。
実際、そのような最適化を行う唯一の方法は、ある種のリンク後のグローバル最適化フェーズによるものです。これらは実行するのが難しいことで知られていますが、一部のコンパイラはある程度実行しています。

于 2009-02-16T03:33:55.660 に答える
2

値渡しには多くの理由があり、コンパイラが意図を最適化すると、コードが壊れる可能性があります。

たとえば、呼び出された関数が何らかの方法で構造を変更する場合。結果を呼び出し元に返すことを意図している場合は、ポインター/参照を渡すか、自分で返す必要があります。

コンパイラに求めているのは、コードの動作を変更することです。これはコンパイラのバグと見なされます。

最適化を行い、参照渡ししたい場合は、誰かの既存の関数/メソッド定義を変更して、参照を受け入れるようにしてください。それほど難しいことではありません。知らず知らずのうちに起こしている破損に驚かれるかもしれません。

于 2009-02-16T03:26:16.130 に答える
1

ささいな答えは、メモリ内の構造体の場所が異なるため、渡すデータが異なるということです。より複雑な答えは、スレッド化だと思います。

コンパイラは、a) foo が構造体を変更しないことを検出する必要があります。b) foo は、構造体要素の物理的な位置について計算を行いません。および c) 呼び出し元、または呼び出し元によって生成された別のスレッドが、foo の実行が完了する前に構造体を変更しない。

あなたの例では、コンパイラがこれらのことを行うことができると考えられますが、保存されたメモリは重要ではなく、おそらく推測する価値はありません。200 万個の要素を持つ構造体で同じプログラムを実行するとどうなるでしょうか?

于 2009-02-16T04:06:21.323 に答える