web-dev-qa-db-ja.com

引数をconst参照として渡すことは時期尚早の最適化ですか?

「時期尚早の最適化はすべての悪の根源です」

これは私たち全員が同意できると思います。そして、私はそれを避けるために一生懸命努力しています。

しかし、最近、パラメータをValueではなくconst Referenceで渡す方法について疑問に思っています。自明ではない関数の引数(つまり、ほとんどの非プリミティブ型)はconst referenceで渡すのが望ましいことを教えられました。 」.

それでも私は疑問に思わずにはいられません:現代のコンパイラーと新しい言語機能は驚くほどうまく機能するので、私が学んだ知識は非常に古くなっているかもしれません。

void fooByValue(SomeDataStruct data);   

そして

void fooByReference(const SomeDataStruct& data);

私が学んだ習慣はconst参照(デフォルトでは重要な型の場合)-早期最適化を渡しますか?

23
CharonX

「時期尚早の最適化」は、最適化を使用することではありませんearly。それは、問題が理解される前、ランタイムが理解される前に最適化することであり、多くの場合、疑わしい結果のためにコードの可読性と保守性を低下させます。

値でオブジェクトを渡す代わりに「const&」を使用することはよく理解された最適化であり、実行時に十分に理解された効果があり、実際には労力がなく、可読性と保守性に悪影響はありません。呼び出しは渡されたオブジェクトを変更しないことを教えてくれるので、実際には両方が改善されます。したがって、コードを書くときに「const&」を追加するのはPREMATUREではありません。

54
gnasher729

TL; DR:C++では、すべてのことを考慮して、const参照による受け渡しをお勧めします。時期尚早の最適化ではありません。

TL; DR2:ほとんどの格言は、理解するまで意味がありません。


目的

この回答は C++ Core Guidelines (amonのコメントで最初に言及された)のリンクされた項目を少し拡張しようとするだけです。

この回答は、プログラマーのサークル内で広く流通しているさまざまな格言を適切に考えて適用する方法の問題、特に矛盾する結論または証拠間の調整の問題に対処しようとするものではありません。


適用性

この回答は、関数呼び出し(同じスレッド上で切り離せないネストされたスコープ)にのみ適用されます。

(補足)。通過可能なものがスコープをエスケープできる(つまり、存続期間が外側のスコープを超える可能性がある)場合、オブジェクトの存続期間管理に対するアプリケーションのニーズを満たすことがより重要になります。何よりもまず。通常、これには、スマートポインターなど、有効期間の管理も可能な参照を使用する必要があります。別の方法として、マネージャーを使用することもできます。ラムダは分離可能なスコープの一種であることに注意してください。ラムダキャプチャは、オブジェクトスコープを持つように動作します。したがって、ラムダキャプチャには注意してください。また、ラムダ自体が渡される方法(コピーまたは参照による)にも注意してください。


値で渡すタイミング

スカラー (マシンレジスター内に収まり、セマンティックな値を持つ標準プリミティブ)であり、可変性による通信(共有参照)が不要な値の場合は、値で渡します。

呼び出し先がオブジェクトまたは集合体の複製を必要とする状況では、呼び出し先のコピーが複製されたオブジェクトの必要を満たす値で渡します。


参照渡しなどの場合

他のすべての状況では、ポインター、参照、スマートポインター、ハンドル(ハンドルボディイディオムを参照)などを渡します。このアドバイスに従う場合は、常にconst-correctnessの原則を適用してください。

メモリフットプリントが十分に大きいもの(集約、オブジェクト、配列、データ構造)は、パフォーマンス上の理由から、常に参照渡しを容易にするように設計する必要があります。このアドバイスは、数百バイト以上の場合に適用されます。このアドバイスは、それが数十バイトであるときは境界線です。


異常なパラダイム

意図的にコピーを多用する特別な目的のプログラミングパラダイムがあります。たとえば、文字列処理、シリアル化、ネットワーク通信、分離、サードパーティライブラリのラッピング、共有メモリのプロセス間通信など。これらのアプリケーション領域またはプログラミングパラダイムでは、データは構造体から構造体にコピーされるか、場合によっては再パッケージされます。バイト配列。


言語仕様がこの回答にどのように影響するか、before最適化が考慮されます。

Sub-TL; DR参照を伝播してもコードは呼び出されません。 const-referenceによる受け渡しはこの基準を満たします。ただし、他のすべての言語はこの基準を簡単に満たします。

(初心者C++プログラマは、このセクションを完全にスキップすることをお勧めします。)

(このセクションの冒頭は、一部gnasher729の回答に触発されています。ただし、別の結論に達しています。)

C++では、ユーザー定義のコピーコンストラクターと代入演算子を使用できます。

(これは(かつて)大胆な選択であり、(かつて)驚くべきことであり、残念なことでもあります。これは、言語設計における今日の許容可能な規範からの逸脱です。)

C++プログラマーが定義しない場合でも、C++コンパイラーは言語の原則に基づいてそのようなメソッドを生成し、memcpy以外の追加コードを実行する必要があるかどうかを判断する必要があります。たとえば、std::vectorメンバーを含むclass/structには、重要なコピーコンストラクターと代入演算子が必要です。

他の言語では、オブジェクトが参照セマンティクスを持っているため、言語の設計により、コピーコンストラクターおよびオブジェクトのクローン作成は推奨されません(アプリケーションのセマンティクスにとって絶対的に必要または意味がある場合を除く)。これらの言語は通常、スコープベースの所有権や参照カウントではなく、到達可能性に基づくガベージコレクションメカニズムを備えています。

参照またはポインター(const参照を含む)がC++(またはC)で渡される場合、プログラマーは、アドレス値の伝搬以外の特別なコード(ユーザー定義関数またはコンパイラー生成関数)が実行されないことが保証されます。 (参照またはポインター)。これは、C++プログラマが快適に感じる動作の明確さです。

ただし、背景はC++言語が不必要に複雑であるため、この明確な動作は核放射性降下物の周辺のどこかにあるオアシス(生存可能な生息地)のようなものです。

より多くの祝福(または侮辱)を追加するために、C++はユニバーサル参照(r値)を導入して、ユーザー定義の移動演算子(移動コンストラクターと移動割り当て演算子)を優れたパフォーマンスで促進します。これは、コピーやディープクローニングの必要性を減らすことで、関連性の高いユースケース(インスタンス間でのオブジェクトの移動(転送))にメリットをもたらします。しかし、他の言語では、そのようなオブジェクトの移動について話すことは非論理的です。


(トピック外のセクション)記事「専用の速度?値渡し!」専用のセクション2009年頃に書かれました。

その記事は2009年に書かれており、C++でのr値の設計正当性について説明しています。その記事は、前のセクションでの私の結論に対する有効な反論を示しています。 ただし、記事のコード例とパフォーマンスの主張は長い間反駁されてきました。

Sub-TL; DRC++でのr値セマンティクスの設計により、Sort関数で驚くほどエレガントなユーザー側セマンティクスが可能になります、 例えば。このエレガントなものを他の言語でモデル化(模倣)することは不可能です。

ソート関数は、データ構造全体に適用されます。上記のように、コピーが大量に含まれる場合は遅くなります。パフォーマンスの最適化(これは実質的に関連性があります)として、ソート関数はC++以外の多くの言語で破壊的になるように設計されています。破壊的とは、ソートの目標を達成するためにターゲットデータ構造が変更されることを意味します。

C++では、ユーザーは2つの実装のうちの1つを呼び出すことを選択できます:より優れたパフォーマンスを持つ破壊的な実装、または入力を変更しない通常の実装。 (テンプレートは簡潔にするために省略されています。)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

並べ替えの他に、この優雅さは、再帰的なパーティション分割によって、配列(最初は並べ替えられていない)に破壊的な中央値検索アルゴリズムを実装する場合にも役立ちます。

ただし、ほとんどの言語では、配列に破壊的なソートアルゴリズムを適用するのではなく、バランスのとれたバイナリサーチツリーアプローチをソートに適用することに注意してください。したがって、この手法の実用的な関連性は見かけほど高くありません。


コンパイラの最適化がこの回答に与える影響

インライン化(​​およびプログラム全体の最適化/リンク時の最適化)がいくつかのレベルの関数呼び出しに適用されると、コンパイラはデータのフローを(場合によっては徹底的に)確認できます。これが発生すると、コンパイラーは多くの最適化を適用できます。その一部は、メモリー内のオブジェクト全体の作成を排除できます。通常、この状況が当てはまる場合、コンパイラーが徹底的に分析できるため、パラメーターが値によって渡されるか、const-referenceによって渡されるかは問題ではありません。

ただし、下位レベルの関数が分析を超えたもの(たとえば、コンパイル外の別のライブラリーの何か、または単に複雑すぎる呼び出しグラフ)を呼び出す場合、コンパイラーは防御的に最適化する必要があります。

マシンレジスタの値よりも大きいオブジェクトは、明示的なメモリのロード/ストア命令、または由緒あるmemcpy関数の呼び出しによってコピーされる場合があります。一部のプラットフォームでは、コンパイラーは2つのメモリー位置間を移動するためにSIMD命令を生成し、各命令は数十バイト(16または32)を移動します。


冗長性または視覚的な混乱の問題に関するディスカッション

C++プログラマーはこれに慣れています。つまり、プログラマーがC++を嫌いでない限り、ソースコード内のconst-referenceの書き込みまたは読み取りのオーバーヘッドは恐ろしいものではありません。

費用便益分析は以前に何度も行われた可能性があります。引用すべき科学的なものがあるかどうかわかりません。ほとんどの分析は非科学的または再現性がないと思います。

ここに私が想像するものがあります(証明または信頼できる参照なしで)...

  • はい、この言語で書かれたソフトウェアのパフォーマンスに影響します。
  • コンパイラーがコードの目的を理解できれば、それを自動化するのに十分賢い可能性があります
  • 残念ながら、(機能的な純粋性とは対照的に)可変性を優先する言語では、コンパイラーはほとんどのものを変異しているものとして分類します。
  • 精神的なオーバーヘッドは人によって異なります。これが精神的なオーバーヘッドが大きいと考える人は、C++を実行可能なプログラミング言語として拒否したでしょう。
16
rwong

DonaldKnuthの論文「StructuredProgrammingWithGoToStatements」で、彼は次のように書いています。 。小さな効率については忘れてください。たとえば、約97%の時間です。時期尚早の最適化がすべての悪の根源です。しかし、その重要な3%で機会を逃してはなりません。」 -時期尚早の最適化

これは、プログラマに利用可能な最も遅いテクニックを使用するように助言するものではありません。それは、プログラムを書くときに明快さに焦点を当てることについてです。多くの場合、明快さと効率はトレードオフです。1つだけを選択する必要がある場合は、明快さを選択してください。しかし、両方を簡単に達成できる場合は、効率を回避するためだけに(何かが一定であることを通知するなど)明快さを損なう必要はありません。

8
Lawrence

([const] [rvalue] reference)|(value)による受け渡しは、インターフェースによって行われた意図と約束についてのものでなければなりません。パフォーマンスとは関係ありません。

リッチーの経験則:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state
8
Richard Hodges

理論的には、答えはイエスです。そして、実際には、時々そうです-実際のところ、渡された値が大きすぎて1つの値に収まらない場合でも、単に値を渡すのではなくconst参照で渡すと、悲観化する可能性があります登録します(または、値で渡すかどうかを判断するために使用する他のヒューリスティックのほとんど)。数年前、David Abrahamsが「Want Speed?Pass by Value!」という名前の記事を書きました。これらのケースのいくつかをカバーしています。見つけるのは簡単ではありませんが、コピーを掘り下げることができれば、読む価値があります(IMO)。

ただし、const参照で渡す特定のケースでは、イディオムが非常によく確立されているため、状況は多少逆転します。知っているでない限り、型はcharになります/ short/int/long、デフォルトではconst参照によって渡されることを期待しているので、特に明確な理由がない限り、それに沿って進むのが最善です。

3
Jerry Coffin