関数の早期復帰の効率


97

これは、経験の浅いプログラマーとして頻繁に遭遇する状況であり、特に最適化しようとしている私の野心的でスピード重視のプロジェクトについて疑問に思っています。主要なCのような言語(C、objC、C ++、Java、C#など)と通常のコンパイラーの場合、これら2つの関数は同じくらい効率的に実行されますか?コンパイルされたコードに違いはありますか?

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}

基本的に、breakINGまたはreturnINGを早くするときに、直接的な効率のボーナス/ペナルティはありますか?スタックフレームはどのように関係していますか?最適化された特別なケースはありますか?これに大きな影響を与える可能性のある要因(インライン化や "Do stuff"のサイズなど)はありますか?

私は常に、マイナーな最適化よりも読みやすさを改善することを支持しています(パラメーターの検証でfoo1がよく見られます)。

そして、私は時期尚早の最適化の落とし穴を知っています...うーん、それらはいくつかのつらい思い出です。

編集:私は答えを受け入れましたが、EJPの答えはaの使用returnが実質的に無視できる理由をかなり簡潔に説明しています(アセンブリでreturnは、関数の終わりに「ブランチ」を作成しますが、これは非常に高速です。ブランチはPCレジスタを変更し、両方のためにも、キャッシュとパイプライン、かなり微小である。)特にこの場合のためにに影響を与える可能性があり、それは文字通り違いはありませんif/elseし、return関数の最後に同じブランチを作成します。


22
そのようなことはパフォーマンスに顕著な影響を与えるとは思いません。小さなテストを書いて、自分自身を見てください。イモ、最初のバリアントは不要なネストを取得しないためより優れており、読み取りが容易になります
SirVaulterScoff

10
@SirVaulterScott。ただし、2つのケースが何らかの形で対称的である場合を除きます。その場合、同じレベルのインデントで対称性を引き出す必要があります。
luqui

3
SirVaulterScoff:不要なネストを削減するための+1
fjdumont 2011年

11
読みやすさ>>>マイクロ最適化。これを維持するウェットウェアにとって、より意味のある方法でそれを実行してください。マシンコードレベルでは、これら2つの構造は、かなり単純なコンパイラーに入力されても同じです。最適化コンパイラは、2つの間の速度上の利点の類似点をすべて消去します。
SplinterReality 2011年

12
このようなことを心配することで、「スピード重視」プロジェクトを最適化しないでください。アプリをプロファイリングして、実際にどこが遅いのかを確認します-動作を完了したときに実際に遅すぎる場合 あなたはほぼ確実に実際にそれを遅くしているものを推測することはできません。
ブルーシフト'25年

回答:


92

まったく違いはありません。

=====> cat test_return.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
    }
    else
        something2();
}
=====> cat test_return2.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
        return;
    }
    something2();
}
=====> rm -f test_return.s test_return2.s
=====> g++ -S test_return.cpp 
=====> g++ -S test_return2.cpp 
=====> diff test_return.s test_return2.s
=====> rm -f test_return.s test_return2.s
=====> clang++ -S test_return.cpp 
=====> clang++ -S test_return2.cpp 
=====> diff test_return.s test_return2.s
=====> 

2つのコンパイラで最適化しなくても、生成されたコードに違いがないことを意味します


59
またはそれ以上:2つのバージョンに対して同じコードを生成する特定のコンパイラのバージョンが少なくとも1つあります。
UncleZeiv 2011年

11
@UncleZeiv-すべてではないにしてもほとんどのコンパイラは、ソースを実行フローグラフモデルに変換します。これらの2つの例に対して意味のある異なるフローグラフを提供する正気な実装を想像するのは困難です。見られる唯一の違いは、2つの異なる処理が入れ替わることです。さらに、分岐予測を最適化するための多くの実装や、プラットフォームが優先順位を決定するその他の問題では、それが取り消されることもあります。
Steve314

6
@ Steve314、確かに、私はちょうどつまらないものだった:)
UncleZeiv

@UncleZeiv:clangでもテストされ、同じ結果
Dani

わかりません。something()常に実行されることは明らかのようです。元の質問では、OPがいるDo stuffDo diffferent stuffフラグによって異なります。生成されたコードが同じになるのは当然です。
Luc M

65

簡単に言えば、違いはありません。自分でお願いして、これについて心配するのをやめてください。最適化コンパイラは、ほとんどの場合、あなたより賢いです。

読みやすさと保守性に集中します。

何が起こるかを確認したい場合は、最適化を有効にしてビルドし、アセンブラーの出力を確認してください。


8
@フィリップ:そして、他の皆にも好意を示し、これについて心配するのをやめてください。あなたが書いたコードは他の人も読んだり保守したりします(他の人が決して読んだことがないように書いたとしても、他の人が読む他のコードに影響を与える習慣がまだあります)。常にコードを可能な限り理解しやすいように記述してください。
hlovdal

8
オプティマイザはあなたより賢くないです!!! それらは、影響がそれほど重要ではない場所を決定する際にのみ高速です。それが本当に重要な場合は、コンパイラよりも最適化された経験があることでしょう。
ヨハネス'25年

10
@johannes同意しない。コンパイラーはアルゴリズムをより良いものに変更しませんが、パイプラインの効率を最大化するために命令を並べ替えたり、ループ(核分裂、核融合など)の些細なことではないので、経験豊富なプログラマーでも決定できません。 CPUアーキテクチャに関する深い知識がない限り、アプリオリの方が適しています。
Fortran、2011年

3
@johannes-この質問については、そうだと思います。また、一般的に、あなたは可能時折、いくつかの特別な場合には、より良いコンパイラより最適化することができるが、これらの日の専門知識の公平なビットを取ること-通常の場合は、オプティマイザはそうあなたが考えることができるほとんどの最適化を適用しないことですいくつかの特別な場合だけではなく、体系的に。WRTこの質問は、コンパイラは、おそらく建設します正確に同じ実行フローグラフの両方のフォームを。より優れたアルゴリズムを選択することは人間の仕事ですが、コードレベルの最適化はほとんど常に時間の無駄です。
Steve314

4
私はこれに同意し、反対します。コンパイラが何かが他のものと同等であることを認識できない場合があります。Uneededブランチが実際に害を与えるx = <some number>よりもif(<would've changed>) x = <some number>はるかに高速であることが多いことをご存知ですか。一方、これが非常に集中的な操作のメインループ内にない限り、私はそれについても心配しません。
user606723 '25 / 10/25

28

興味深い答え:私はそれらのすべてに(今のところ)同意しますが、今のところ完全に無視されている可能性のあるこの質問への示唆があります。

上記の簡単な例をリソース割り当てで拡張し、エラーチェックを行ってリソースが解放される可能性がある場合は、状況が変わる可能性があります。

初心者が取るかもしれない素朴なアプローチを検討してください:

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  if (!a) {
    return 1;
  }
  res_b b = allocate_resource_b();
  if (!b) {
    free_resource_a(a);
    return 2;
  }
  res_c c = allocate_resource_c();
  if (!c) {
    free_resource_b(b);
    free_resource_a(a);
    return 3;
  }

  do_work();

  free_resource_c(c);
  free_resource_b(b);
  free_resource_a(a);

  return 0;
}

上記は時期尚早に戻るスタイルの極端なバージョンを表しています。コードの複雑さが増すと、コードが時間の経過とともに非常に繰り返しやすくなり、保守できなくなることに注意してください。今日、人々はこれらをキャッチするために例外処理を使用するかもしれません。

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  try {
    a = allocate_resource_a(); # throws ExceptionResA
    b = allocate_resource_b(); # throws ExceptionResB
    c = allocate_resource_c(); # throws ExceptionResC
    do_work();
  }  
  catch (ExceptionBase e) {
    # Could use type of e here to distinguish and
    # use different catch phrases here
    # class ExceptionBase must be base class of ExceptionResA/B/C
    if (c) free_resource_c(c);
    if (b) free_resource_b(b);
    if (a) free_resource_a(a);
    throw e
  }
  return 0;
}

フィリップは、以下のgotoの例を調べた後、上のcatchブロック内でブレークなしのスイッチ/ケースを使用することを提案しました。switch(typeof(e))をfree_resourcex()呼び出してから、呼び出しに失敗する可能性がありますが、これは簡単なことではなく、設計上の考慮が必要です。そして、切れ目のないスイッチ/ケースは、以下のデイジーチェーンされたラベルのあるgotoとまったく同じです...

Mark Bが指摘したように、C ++では、リソースの取得は初期化の原則であるRAIIに従うのが適切なスタイルと見なされます。つまりに。コンセプトの要点は、オブジェクトのインスタンス化を使用してリソースを取得することです。その後、オブジェクトがスコープ外になり、そのデストラクタが呼び出されるとすぐに、リソースは自動的に解放されます。相互に依存するリソースについては、割り当て解除の正しい順序を保証し、必要なデータがすべてのデストラクタで利用できるようにオブジェクトのタイプを設計するために、特別な注意を払う必要があります。

または、例外前の日に行う可能性があります:

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  res_b b = allocate_resource_b();
  res_c c = allocate_resource_c();
  if (a && b && c) {   
    do_work();
  }  
  if (c) free_resource_c(c);
  if (b) free_resource_b(b);
  if (a) free_resource_a(a);

  return 0;
}

ただし、この過度に単純化された例にはいくつかの欠点があります。割り当てられたリソースが相互に依存しない場合にのみ使用できます(たとえば、メモリの割り当てに使用できず、ファイルハンドルを開いて、ハンドルからメモリにデータを読み取ります) )、戻り値として個別の識別可能なエラーコードを提供しません。

コードを高速(!)、コンパクト、簡単に読み取り、拡張できるようにするために、Linus Torvaldsは、悪名高いgotoを絶対的に意味のある方法で使用していても、リソースを処理するカーネルコードに異なるスタイルを適用しました

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  a = allocate_resource_a() || goto error_a;
  b = allocate_resource_b() || goto error_b;
  c = allocate_resource_c() || goto error_c;

  do_work();

error_c:
  free_resource_c(c);
error_b:
  free_resource_b(b);
error_a:
  free_resource_a(a);

  return 0;
}

カーネルメーリングリストに関する議論の要点は、gotoステートメントよりも「推奨される」ほとんどの言語機能が、巨大なツリーのようなif / else、例外ハンドラー、ループ/ブレーク/継続ステートメントなどの暗黙のゴトであることです。 。上記の例のgotoは、ジャンプする距離が短く、明確なラベルがあり、エラー状態を追跡するために他の混乱のコードを解放するため、問題ありません。この質問については、stackoverflowでも説明しています。

ただし、前の例で欠けているのは、エラーコードを返す良い方法です。result_code++それぞれにアフターを追加することを考えていましたfree_resource_x()呼び出しのそのコードを返すこといましたが、これは上記のコーディングスタイルの速度向上の一部を相殺します。また、成功した場合に0を返すのは困難です。たぶん私は想像を絶するだけです;-)

だから、はい、時期尚早のリターンのコーディングの問題には大きな違いがあると思います。しかし、私はまた、コンパイラー用に再構成して最適化することがより困難または不可能である、より複雑なコードでのみ明らかになると思います。これは通常、リソース割り当てが機能するようになった場合に当てはまります。


1
うわー、本当に面白い。素朴なアプローチの保守性に間違いなく感謝します。その特定のケースで例外処理はどのように改善されますか?同様catchブレークレス含むswitchエラーコード上の文を?
Philip Guin、2011年

@Philip基本的な例外処理の例を追加しました。gotoのみがフォールスルーの可能性があることに注意してください。提案されたswitch(typeof(e))は役立ちますが、簡単でなく、設計の考慮が必要です。また、ブレークのないスイッチ/ケースは、デイジーチェーンラベルを使用したgotoとまったく同じです。;-)
cfi

+1これはC / C ++(または手動でメモリを解放する必要がある言語)の正しい答えです。個人的には、マルチレーベルバージョンは好きではありません。私の以前の会社では、それは常に「後藤」でした(それはフランスの会社でした)。最後に、メモリの割り当てを解除します。コードレビューに合格するgotoはこれだけです。
キップ、

1
C ++では、これらのアプローチを実行しませんが、RAIIを使用してリソースが適切にクリーンアップされることを確認します。
マークB

12

これはあまり答えにはなりませんが、実稼働コンパイラは最適化に関してはあなたより優れています。これらの種類の最適化よりも可読性と保守性を優先します。


9

これについて具体的に言うreturnと、はメソッドの最後への分岐にコンパイルされRETます。そこでは、命令またはそれが何であれ、そこにあります。省略した場合、ブロックの前のブロックの終わりは、ブロックの終わりelseへの分岐にコンパイルされelseます。したがって、この特定のケースでは、何の違いもないことがわかります。


ゴッチャ。私はこれが私の質問にかなり簡潔に答えると思います。文字通り単なるレジスターの追加だと思いますが、これはごくわずかです(システムプログラミングを行っている場合を除いて...)。
Philip Guin、2011年

@フィリップ何レジスタ追加?パスに余分な命令はまったくありません。
ローン侯爵、

まあ両方ともレジスタの追加があります。アセンブリブランチはこれだけですよね。プログラムカウンターへの追加?私はここで間違っている可能性があります。
Philip Guin、2011年

1
@フィリップいいえ、組み立てブランチは組み立てブランチです。それはもちろん、PCには影響しないが、それは完全にそれを再ロードすることによって可能性があり、それはまた、パイプラインWRTプロセッサにおける副作用、キャッシュなどを持っている
ローン侯爵

4

特定のコンパイラとシステムのコンパイル済みコードに違いがあるかどうかを本当に知りたい場合は、自分でアセンブリをコンパイルして確認する必要があります。

ただし、大きなスキームでは、コンパイラーが微調整よりも最適化できることがほぼ確実であり、それができない場合でも、プログラムのパフォーマンスに実際に影響する可能性はほとんどありません。

代わりに、人間が読み取って保守できる最もわかりやすい方法でコードを記述し、コンパイラーが最善を尽くすようにします。ソースから可能な限り最高のアセンブリーを生成します。


4

あなたの例では、リターンは顕著です。戻り値が//さまざまなことが発生するページの1つまたは2つ上または下である場合、デバッグしている人はどうなりますか?より多くのコードがあると、見つける/見るのがはるかに難しくなります。

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}

もちろん、関数は1ページ(または2ページ)を超えてはなりません。しかし、デバッグの側面は、他のどの回答でもまだカバーされていません。ポイント獲得!
cfi '10 / 10/26

3

私はblueshiftに強く同意します。しかし、本当に心配している場合(またはコンパイラーが何をしているかを知りたい場合は、長期的には間違いなく良い考えです)、自分を探す必要があります。

これは、逆コンパイラーを使用するか、低レベルのコンパイラー出力(例えば、アセンブリ言語)を調べることを意味します。C#または任意の.Net言語では、ここ記載されているツールが必要なものを提供します。

しかし、あなた自身が観察したように、これはおそらく時期尚早の最適化です。


1

クリーンコードから:アジャイルソフトウェアの職人技のハンドブック

フラグの引数は醜いです。ブール値を関数に渡すことは本当にひどい習慣です。メソッドのシグネチャをすぐに複雑にし、この関数が複数のことを行うことを大声で宣言します。フラグが真の場合は1つの処理を行い、フラグが偽の場合は別の処理を実行します。

foo(true);

コードでは、リーダーが関数に移動し、foo(booleanフラグ)を読み取る時間を無駄にするだけです。

構造化されたコードベースの改善により、コードを最適化する機会が増えます。


これは例として使用しています。関数に渡されるのは、int、double、クラスなどですが、実際には問題の中心にはありません。
Philip Guin、2011年

あなたが尋ねた質問はあなたの関数の中でスイッチをすることについてです、ほとんどの場合、それはコードのにおいです。それは多くの方法で達成でき、読者はその関数全体を読む必要はありません、foo(28)はどういう意味ですか?

0

(現時点で提案したeggheadを思い出せない)考え方の1つは、コードを読みやすく、デバッグしやすくするために、すべての関数に構造的な観点からの戻り点が1つだけあるべきだということです。それはおそらく、宗教的な議論をプログラミングするためのものです。

このルールに違反する関数がいつ、どのように終了するかを制御する必要がある技術的理由の1つは、リアルタイムアプリケーションをコーディングしていて、関数を通るすべての制御パスが同じ数のクロックサイクルを完了して完了するようにする場合です。


ええと、それはクリーンアップ(Cでコーディングするときのesp)に関係していると思いました。
Thomas Eding、2011年

いいえ、スタックを返す限り、メソッドをどこに置いても、スタックが元に戻されます(つまり、「クリーンアップ」されます)。
MartyTPS 2011年

-4

この質問をお持ちいただきありがとうございます。早期の復帰では常にブランチを使用する必要があります。なぜそこに止まるのですか?できれば(少なくともできるだけ)すべての関数を1つにマージします。再帰がなければこれは可能です。結局、あなたは1つの大きなメイン機能を持つでしょうが、それはあなたがこの種のもののために必要/望んでいるものです。その後、識別子の名前をできるだけ短くします。これにより、コードが実行されるときに、名前の読み取りに費やす時間が短縮されます。次は...


3
冗談を言っているのかもしれませんが、怖いのは、あなたのアドバイスを真剣に受け止めている人もいるということです。
Daniel Pryden、2011年

ダニエルに同意します。私が皮肉を愛するのと同じくらい-それは技術文書、ホワイトペーパー、SOのようなQ&Aサイトで使用されるべきではありません。
cfi 2011年

1
皮肉な答えの場合は-1。初心者には必ずしも認識されない。
ヨハンベゼム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.