web-dev-qa-db-ja.com

関数内の早期リターンの効率

これは、経験の浅いプログラマーとして頻繁に遭遇する状況であり、特に、最適化しようとしている私の野心的で速度を重視するプロジェクトについて疑問に思っています。主要な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の早い段階で、直接的な効率のボーナス/ペナルティはありますか?スタックフレームはどのように関係していますか?最適化された特別なケースはありますか?これに大きな影響を与える可能性のある要因(インライン化や「Dostuff」のサイズなど)はありますか?

私は常にマイナーな最適化よりも読みやすさを改善することを支持しています(パラメーターの検証でfoo1がよく見られます)が、これは頻繁に発生するため、すべての心配を一度に取っておきたいと思います。

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

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

96
Philip

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

=====> 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つのコンパイラで最適化を行わなくても、生成されたコードに違いがないことを意味します

92
Dani

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

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

何が起こるかを確認したい場合は、最適化を使用してこれらを構築し、アセンブラーの出力を確認してください。

65
blueshift

興味深い答え:私は(これまでのところ)それらすべてに同意しますが、これまで完全に無視されてきたこの質問への含意が考えられます。

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

ナイーブなアプローチ初心者が取るかもしれないことを考えてください:

_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の例を見た後、上のキャッチブロック内でブレークレススイッチ/ケースを使用することを提案しました。 switch(typeof(e))を実行してから、free_resourcex()呼び出しに失敗する可能性がありますが、これは 些細なことではなく、設計上の考慮が必要 です。そして、切れ目のないスイッチ/ケースは、下にデイジーチェーン接続されたラベルが付いた後藤とまったく同じであることを忘れないでください...

Mark Bが指摘したように、C++では、Resource Aquisition is Initializationの原則に従うのが良いスタイルと考えられています [〜#〜] 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、例外ハンドラー、loop/break/continueステートメントなどの暗黙のgotoであるということです。 。そして、上記の例のgotoは、わずかな距離しかジャンプせず、明確なラベルがあり、エラー状態を追跡するために他の混乱のコードを解放するため、問題ないと見なされます。 この質問はstackoverflowでもここで議論されています

ただし、最後の例で欠落しているのは、エラーコードを返すための優れた方法です。各free_resource_x()呼び出しの後に_result_code++_を追加し、そのコードを返すことを考えていましたが、これは上記のコーディングスタイルの速度向上の一部を相殺します。そして、成功した場合に0を返すのは難しいです。多分私は想像を絶するだけです;-)

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

28
cfi

これはあまり答えではありませんが、実動コンパイラーは、あなたよりもはるかに優れた最適化を実現します。私は、これらの種類の最適化よりも読みやすさと保守性を優先します。

12
Lou

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

9
user207421

あなたの例では、リターンが目立ちます。 //異なることが発生する場所の上下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
    }
}
4
PCPGMR

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

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

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

4
Mark B

私はブルーシフトに強く同意します:読みやすさと保守性を最初に!しかし、本当に心配している場合(または、コンパイラが何をしているのかを知りたいだけの場合、長期的には間違いなく良い考えです)、自分で探す必要があります。

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

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

3
Mr. Putty

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

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

foo(true);

コード内では、リーダーが関数に移動し、foo(ブールフラグ)を読み取るのに時間を浪費するだけです。

構造化されたコードベースが優れていると、コードを最適化する機会が増えます。

1
Yuan

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

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

0
MartyTPS