constが早すぎる最適化を参照するときに引数を渡しますか?


20

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

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

しかし最近、私はValueではなくconst Referenceによってパラメーターを渡す方法について疑問に思っています。非自明な関数引数(つまり、ほとんどの非プリミティブ型)はconst参照で渡すことが望ましいことを教えられました。これまで読んだ本の中では、これを「ベストプラクティス」として推奨しているものがかなりあります。

それでも私は不思議に思わずにはいられません:現代のコンパイラーと新しい言語機能は驚異的に機能する可能性があるため、私が学んだ知識は非常に古くなっている可能性があり、

void fooByValue(SomeDataStruct data);   

そして

void fooByReference(const SomeDataStruct& data);

私が学んだ慣習は-const参照を渡す(非自明な型のデフォルト)-早すぎる最適化ですか?


1
さまざまなパラメーターの受け渡し方法については、C ++コアガイドラインのF.callも参照してください。
アモン


1
@DocBrownその質問に対する受け入れられた答えは、ここでも当てはまるかもしれない最小限の驚きの原則を指します(つまり、const参照の使用は業界標準などです)。とはいえ、この質問が重複していることに同意しません。あなたが言及する質問は、コンパイラの最適化に(一般的に)頼るのが悪い習慣かどうかを尋ねます。この質問は逆を求めます:const参照を渡すことは(時期尚早な)最適化ですか?
CharonX

@CharonX:ここでコンパイラの最適化に頼ることができる場合、あなたの質問に対する答えは明らかに「はい、手動の最適化は不要で、時期尚早です」です。信頼できない場合(おそらく、どのコンパイラーがコードに使用されるか事前にわからないため)、答えは「より大きなオブジェクトの場合、おそらく時期尚早ではない」です。したがって、これらの2つの質問が文字通り等しくない場合でも、それらは重複としてリンクするのに十分似ているようです。
Doc Brown

1
@DocBrown:だから、それをitと宣言する前に、質問のどこでコンパイラがそれを許可し、それを「最適化」できると言っているかを指摘してください。
デュプリケータ

回答:


49

「時期尚早な最適化」とは、最適化を早期に使用することではありません。問題を理解する前、ランタイムを理解する前に最適化を行い、多くの場合、疑わしい結果に対してコードを読みにくく、保守しにくくします。

オブジェクトを値で渡す代わりに「const&」を使用することは、実行時によく理解される最適化であり、実際には労力がかからず、読みやすさと保守性に悪影響を与えません。呼び出しが渡されたオブジェクトを変更しないことを教えてくれるので、実際に両方を改善します。そのため、コードを記述するときに「const&」を追加することは早急ではありません。


2
あなたの答えの「実質的に努力なし」の部分に同意します。ただし、パフォーマンスの著しい影響が測定される前に、時期尚早の最適化が何よりもまず最適化について行われます。そして、私はほとんどのC ++プログラマー(自分を含む)がを使用する前に測定を行うとは思わないconst&ので、質問は非常に理にかなっていると思います。
ドックブラウン

1
最適化する前に測定して、トレードオフに価値があるかどうかを判断します。const&を使用すると、合計で7文字を入力するだけで済み、他にも利点があります。渡される変数を変更するつもりがない場合、速度の改善がなくても有利です。
gnasher729

3
私はCの専門家ではないので、質問:const& foo関数はfooを変更しないので、呼び出し元は安全だと言います。しかし、コピーされた値は、他のスレッドがfooを変更できないことを示しているため、呼び出し先は安全です。正しい?そのため、マルチスレッドアプリでは、答えは最適化ではなく正確さに依存します。
user949300

1
@DocBrown最終的には、const&を入れた開発者の動機について質問することができます。残りを考慮せずにパフォーマンスのみのためにそれを行った場合、それは時期尚早な最適化と見なされる可能性があります。今では、constパラメーターであることがわかっているためにそれを置くと、コードを自己文書化し、コンパイラーに最適化する機会を与えるだけです。
ウォルフラット

1
@ user949300:同時に、または使用されるコールバックによって引数を変更できる関数はほとんどありません。
デュプリケータ

16

TL; DR:C ++では、const参照による受け渡しは今でも良い考えです。時期尚早な最適化ではありません。

TL; DR2:ほとんどの格言は、意味があるまで意味をなさない。


目的

この回答は、C ++コアガイドライン(amonのコメントで最初に言及された)のリンクされた項目を少し拡張しようとするものです。

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


適用性

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

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


値渡しする場合

ある値に対してスカラーれる通信ごとに可変性(共有参照)を必要としない(マシン・レジスタ内に収まる値セマンティックを有する標準プリミティブ)、値渡し。

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


参照渡しする場合など

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

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


異常なパラダイム

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


最適化が検討される前に、言語仕様がこの回答にどのように影響するか。

Sub-TL; DR参照の伝播では、コードを呼び出さないでください。const-referenceを渡すと、この基準が満たされます。ただし、他のすべての言語はこの基準を簡単に満たします。

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

(このセクションの冒頭はgnasher729の回答に一部影響を受けています。ただし、別の結論に達しました。)

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

(これは(かつて)大胆な選択でした(驚くべきことでしたし、後悔していました。これは間違いなく、言語設計における今日の許容範囲からの逸脱です。)

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

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

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

しかし、背景は、C ++言語が不必要に複雑であるため、このような明確な動作は、核フォールアウトゾーン周辺のオアシス(生存可能な生息地)のようなものです。

祝福(またはin辱)を追加するために、C ++では、ユーザー定義の移動演算子(move-constructorsおよびmove-assignment演算子)のパフォーマンスを向上させるために、ユニバーサルリファレンス(r-values)を導入しています。これは、コピーとディープクローニングの必要性を減らすことにより、関連性の高いユースケース(1つのインスタンスから別のインスタンスへのオブジェクトの移動(転送))に役立ちます。ただし、他の言語では、このようなオブジェクトの移動について話すのは非論理的です。


(トピック外のセクション)「スピードを望みますか?値渡し!」という記事専用のセクション。2009年頃に書かれました。

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

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

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

C ++では、ユーザーは、2つの実装のいずれかを呼び出すことができます。パフォーマンスが向上した破壊的な実装、または入力を変更しない通常の実装です。(簡潔にするためにテンプレートは省略されています。)

/*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参照の書き込みまたは読み取りのオーバーヘッドは恐ろしくありません。

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

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

  • はい、この言語で書かれたソフトウェアのパフォーマンスに影響します。
  • コンパイラーがコードの目的を理解できれば、それを潜在的に自動化するのに十分に賢い可能性があります。
  • 残念ながら、(機能的な純度ではなく)可変性を好む言語では、コンパイラはほとんどのものを変異していると分類するため、constnessの自動推論はほとんどのものを非constとして拒否します
  • 精神的なオーバーヘッドは人によって異なります。これが大きな頭痛の種だと思う人は、C ++を実行可能なプログラミング言語として拒否したでしょう。

これは、1つだけを選択する代わりに2つの回答を受け入れることができる状況の1つです... ため息
-CharonX

8

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

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


3
「1つだけ選択する必要がある場合は、明快さを選択してください。」代わりに2番目のを選択する必要があります。
デュプリケータ

@Deduplicatorありがとうございます。ただし、OPのコンテキストでは、プログラマーは自由に選択できます。
ローレンス

あなたの答えは...もう少し一般的なしかしそれよりも読み込み
デュプリケータ

@Deduplicatorああ、しかし、私の答えの文脈は、プログラマーが(また)選択することです。選択がプログラマーに強制された場合、ピッキングを行うのは「あなた」ではありません:)。私はあなたが提案した変更を考慮し、それに応じて私の答えを編集することに反対しませんが、明確にするために既存の文言を好みます。
ローレンス

7

([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

3

理論的には、答えはイエスです。そして、実際には、いくつかの時間です-実際のところ、渡された値が大きすぎて単一に収まらない場合でも、値を渡すだけでなくconst参照で渡すことは悲観的です登録(または、値渡しするかどうかを決定するために使用しようとする他のヒューリスティックのほとんど)。数年前、デビッド・アブラハムスは「Want Speed?Pass by Value!」という記事を書きました。これらのケースの一部をカバーしています。見つけるのは簡単ではありませんが、コピーを掘り下げることができれば、読む価値があります(IMO)。

const参照渡しの特定のケースでは、しかし、私はイディオムがとてもよく状況が多かれ少なかれ逆転されることが確立されて言うと思います:あなたがない限り、知っているタイプがされますchar/ short/ int/ long、人々はそれがconstので渡さ見ることを期待しますデフォルトで参照するので、かなり特別な理由がない限り、おそらく一緒に使うのが最善です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.