23

私は最近ハーブサッターによる「例外的なC++」を通り抜けていました、そして私は彼がアイテム6-一時的なオブジェクトで与える特定の推薦について深刻な疑いを持っています。

彼は、次のコードで不要な一時オブジェクトを見つけることを提案しています。

string FindAddr(list<Employee> emps, string name) 
{
  for (list<Employee>::iterator i = emps.begin(); i != emps.end(); i++)
  {
    if( *i == name )
    {
      return i->addr;
    }
  }
  return "";
}

emps.end()例の1つとして、反復ごとに一時オブジェクトが作成されるため、ループの前の値を事前に計算することをお勧めします。

ほとんどのコンテナ(リストを含む)の場合、end()を呼び出すと、構築および破棄する必要のある一時オブジェクトが返されます。値は変更されないため、ループの反復ごとに値を再計算(および再構築して再破棄)することは、不必要に非効率的であり、美的ではありません。値は1回だけ計算し、ローカルオブジェクトに保存して、再利用する必要があります。

そして彼は次のように置き換えることを提案します:

list<Employee>::const_iterator end(emps.end());
for (list<Employee>::const_iterator i = emps.begin(); i != end; ++i)

私にとって、これは不必要な合併症です。醜い型宣言をcompactに置き換えても、1auto行ではなく2行のコードを取得します。さらに、彼はこのend変数を外部スコープに持っています。

私は実際にconst_iteratorここで使用しており、ループのコンテンツが何らかの形でコンテナにアクセスしているかどうかを簡単に確認できるため、最近のコンパイラはこのコードを最適化すると確信していました。コンパイラは過去13年以内に賢くなったでしょう?

とにかく、私はi != emps.end()ほとんどの場合、パフォーマンスについてそれほど心配していない最初のバージョンを好みます。しかし、私は確かに、これが最適化するためにコンパイラに頼ることができる一種の構造であるかどうかを知りたいですか?

アップデート

この役に立たないコードをより良くする方法についてのあなたの提案に感謝します。私の質問は、プログラミング技術ではなく、コンパイラに関するものであることに注意してください。今のところ、関連する唯一の回答はNPEElliohからのものです。

4

7 に答える 7

12

UPD:あなたが話している本は、私が間違えない限り、1999年に出版されました。それは14年前のことであり、現代のプログラミングでは14年はかなりの時間です。1999年に優れていて信頼できる多くの推奨事項は、今では完全に廃止されている可能性があります。私の答えは単一のコンパイラと単一のプラットフォームに関するものですが、より一般的な考え方もあります。

余分な変数を気にし、些細なメソッドの戻り値や古いC ++の同様のトリックを再利用することは、1990年代のC++への一歩です。のような些細なメソッドend()は非常にうまくインライン化する必要があり、インライン化の結果は、それが呼び出されるコードの一部として最適化する必要があります。end99%の状況では、変数の作成などの手動アクションはまったく必要ありません。このようなことは、次の場合にのみ実行する必要があります。

  1. 一部のコンパイラ/プラットフォームでは、コードで実行する必要があるものが適切に最適化されていないことをご存知でしょう。
  2. これは、プログラムのボトルネックになっています(「時期尚早の最適化を回避する」)。

私は64ビットg++によって生成されるものを見てきました:

gcc version 4.6.3 20120918 (prerelease) (Ubuntu/Linaro 4.6.3-10ubuntu1)

当初、最適化を行うことで問題はなく、2つのバージョンに違いはないはずだと思いました。しかし、物事は奇妙に見えます。最適ではないと見なしたバージョンの方が実際には優れています。私が思うに、道徳は次のとおりです。コンパイラよりも賢くしようとする理由はありません。両方のバージョンを見てみましょう。

#include <list>

using namespace std;

int main() {
  list<char> l;
  l.push_back('a');

  for(list<char>::iterator i=l.begin(); i != l.end(); i++)
      ;

  return 0;
}

int main1() {
  list<char> l;
  l.push_back('a');
  list<char>::iterator e=l.end();
  for(list<char>::iterator i=l.begin(); i != e; i++)
      ;

  return 0;
}

次に、これを最適化してコンパイルし(64ビットを使用しg++ます。コンパイラを試してみてください)、逆アセンブルmainしてmain1

の場合main

(gdb) disas main
Dump of assembler code for function main():
   0x0000000000400650 <+0>: push   %rbx
   0x0000000000400651 <+1>: mov    $0x18,%edi
   0x0000000000400656 <+6>: sub    $0x20,%rsp
   0x000000000040065a <+10>:    lea    0x10(%rsp),%rbx
   0x000000000040065f <+15>:    mov    %rbx,0x10(%rsp)
   0x0000000000400664 <+20>:    mov    %rbx,0x18(%rsp)
   0x0000000000400669 <+25>:    callq  0x400630 <_Znwm@plt>
   0x000000000040066e <+30>:    cmp    $0xfffffffffffffff0,%rax
   0x0000000000400672 <+34>:    je     0x400678 <main()+40>
   0x0000000000400674 <+36>:    movb   $0x61,0x10(%rax)
   0x0000000000400678 <+40>:    mov    %rax,%rdi
   0x000000000040067b <+43>:    mov    %rbx,%rsi
   0x000000000040067e <+46>:    callq  0x400610 <_ZNSt8__detail15_List_node_base7_M_hookEPS0_@plt>
   0x0000000000400683 <+51>:    mov    0x10(%rsp),%rax
   0x0000000000400688 <+56>:    cmp    %rbx,%rax
   0x000000000040068b <+59>:    je     0x400698 <main()+72>
   0x000000000040068d <+61>:    nopl   (%rax)
   0x0000000000400690 <+64>:    mov    (%rax),%rax
   0x0000000000400693 <+67>:    cmp    %rbx,%rax
   0x0000000000400696 <+70>:    jne    0x400690 <main()+64>
   0x0000000000400698 <+72>:    mov    %rbx,%rdi
   0x000000000040069b <+75>:    callq  0x400840 <std::list<char, std::allocator<char> >::~list()>
   0x00000000004006a0 <+80>:    add    $0x20,%rsp
   0x00000000004006a4 <+84>:    xor    %eax,%eax
   0x00000000004006a6 <+86>:    pop    %rbx
   0x00000000004006a7 <+87>:    retq   

0x0000000000400683-0x000000000040068bにあるコマンドを見てください。これがループ本体であり、完全に最適化されているようです。

   0x0000000000400690 <+64>:    mov    (%rax),%rax
   0x0000000000400693 <+67>:    cmp    %rbx,%rax
   0x0000000000400696 <+70>:    jne    0x400690 <main()+64>

の場合main1

(gdb) disas main1
Dump of assembler code for function main1():
   0x00000000004007b0 <+0>: push   %rbp
   0x00000000004007b1 <+1>: mov    $0x18,%edi
   0x00000000004007b6 <+6>: push   %rbx
   0x00000000004007b7 <+7>: sub    $0x18,%rsp
   0x00000000004007bb <+11>:    mov    %rsp,%rbx
   0x00000000004007be <+14>:    mov    %rsp,(%rsp)
   0x00000000004007c2 <+18>:    mov    %rsp,0x8(%rsp)
   0x00000000004007c7 <+23>:    callq  0x400630 <_Znwm@plt>
   0x00000000004007cc <+28>:    cmp    $0xfffffffffffffff0,%rax
   0x00000000004007d0 <+32>:    je     0x4007d6 <main1()+38>
   0x00000000004007d2 <+34>:    movb   $0x61,0x10(%rax)
   0x00000000004007d6 <+38>:    mov    %rax,%rdi
   0x00000000004007d9 <+41>:    mov    %rsp,%rsi
   0x00000000004007dc <+44>:    callq  0x400610 <_ZNSt8__detail15_List_node_base7_M_hookEPS0_@plt>
   0x00000000004007e1 <+49>:    mov    (%rsp),%rdi
   0x00000000004007e5 <+53>:    cmp    %rbx,%rdi
   0x00000000004007e8 <+56>:    je     0x400818 <main1()+104>
   0x00000000004007ea <+58>:    mov    %rdi,%rax
   0x00000000004007ed <+61>:    nopl   (%rax)
   0x00000000004007f0 <+64>:    mov    (%rax),%rax
   0x00000000004007f3 <+67>:    cmp    %rbx,%rax
   0x00000000004007f6 <+70>:    jne    0x4007f0 <main1()+64>
   0x00000000004007f8 <+72>:    mov    (%rdi),%rbp
   0x00000000004007fb <+75>:    callq  0x4005f0 <_ZdlPv@plt>
   0x0000000000400800 <+80>:    cmp    %rbx,%rbp
   0x0000000000400803 <+83>:    je     0x400818 <main1()+104>
   0x0000000000400805 <+85>:    nopl   (%rax)
   0x0000000000400808 <+88>:    mov    %rbp,%rdi
   0x000000000040080b <+91>:    mov    (%rdi),%rbp
   0x000000000040080e <+94>:    callq  0x4005f0 <_ZdlPv@plt>
   0x0000000000400813 <+99>:    cmp    %rbx,%rbp
   0x0000000000400816 <+102>:   jne    0x400808 <main1()+88>
   0x0000000000400818 <+104>:   add    $0x18,%rsp
   0x000000000040081c <+108>:   xor    %eax,%eax
   0x000000000040081e <+110>:   pop    %rbx
   0x000000000040081f <+111>:   pop    %rbp
   0x0000000000400820 <+112>:   retq   

ループのコードは似ています、それは次のとおりです。

   0x00000000004007f0 <+64>:    mov    (%rax),%rax
   0x00000000004007f3 <+67>:    cmp    %rbx,%rax
   0x00000000004007f6 <+70>:    jne    0x4007f0 <main1()+64>

しかし、ループの周りにはたくさんの余分なものがあります。どうやら、余分なコードは物事を悪化させました。

于 2013-03-15T13:47:10.967 に答える
8

g++ 4.7.2を使用して次の少しハッキーなコードをコンパイルし、-O3 -std=c++11両方の関数で同じアセンブリを取得しました。

#include <list>
#include <string>

using namespace std;

struct Employee: public string { string addr; };

string FindAddr1(list<Employee> emps, string name)
{
  for (list<Employee>::const_iterator i = emps.begin(); i != emps.end(); i++)
  {
    if( *i == name )
    {
      return i->addr;
    }
  }
  return "";
}

string FindAddr2(list<Employee> emps, string name)
{
  list<Employee>::const_iterator end(emps.end());
  for (list<Employee>::const_iterator i = emps.begin(); i != end; i++)
  {
    if( *i == name )
    {
      return i->addr;
    }
  }
  return "";
}

いずれにせよ、2つのバージョンのどちらを選択するかは、主に読みやすさの理由に基づいて行う必要があると思います。データをプロファイリングしないと、このようなマイクロ最適化は時期尚早に見えます。

于 2013-03-15T13:22:36.410 に答える
4

一般に信じられていることとは反対に、この点でVC++とgccの間に違いは見られません。g++4.7.2とMSC++ 17(別名VC ++ 2012)の両方で簡単なチェックを行いました。

どちらの場合も、質問のコード(コンパイルするためにヘッダーなどが追加されている)で生成されたコードを次のコードと比較しました。

string FindAddr(list<Employee> emps, string name) 
{
    auto end = emps.end();
    for (list<Employee>::iterator i = emps.begin(); i != end; i++)
    {
        if( *i == name )
        {
            return i->addr;
        }
    }
    return "";
}

どちらの場合も、結果は2つのコードで本質的に同じでした。VC ++のコードには行番号のコメントが含まれていますが、これは余分な行のために変更されましたが、それが唯一の違いでした。g ++では、出力ファイルは同じでした。

std::vectorの代わりに同じことを行うと、std::listほぼ同じ結果が得られました。大きな違いはありません。何らかの理由で、g ++は1つの命令のオペランドの順序をからに切り替えましたcmp esi, DWORD PTR [eax+4]cmp DWORD PTR [eax+4], esi、(これも)まったく関係ありません。

結論:いいえ、最新のコンパイラを使用してコードをループから手動で引き上げても何も得られない可能性があります(少なくとも最適化が有効になっている場合-私は/O2b2VC++と/O3g++で使用していました;最適化と最適化をオフにしたものを比較するようです私にはかなり無意味です)。

于 2013-03-15T14:59:46.940 に答える
3

いくつかのこと...1つ目は、一般に、イテレータ(リリースモードでは、チェックされていないアロケータ)を構築するコストが最小限であるということです。それらは通常、ポインタのラッパーです。チェックされたアロケータ(VSのデフォルト)では、いくらかのコストがかかる可能性がありますが、本当にパフォーマンスが必要な場合は、チェックされていないアロケータで再構築をテストした後。

コードは、投稿したものほど醜いものである必要はありません。

for (list<Employee>::const_iterator it=emps.begin(), end=emps.end(); 
                                    it != end; ++it )

どちらのアプローチを使用するかについての主な決定は、コンテナーに適用されている操作の観点から行う必要があります。コンテナーのサイズが変更されている可能性がある場合は、end反復ごとにイテレーターを再計算することをお勧めします。そうでない場合は、1回だけ事前計算して、上記のコードのように再利用できます。

于 2013-03-15T13:16:07.777 に答える
3

本当にパフォーマンスが必要な場合は、光沢のある新しいC++11コンパイラに次のように記述させます。

for (const auto &i : emps) {
    /* ... */
}

はい、これは冗談です(一種の)。ここでのハーブの例は今では時代遅れです。しかし、コンパイラはまだそれをサポートしていないので、本当の質問に取り掛かりましょう。

これは、最適化するためにコンパイラーに頼ることができる一種の構造ですか?

私の親指のルールは、コンパイラの作成者は私よりもはるかに賢いということです。コンパイラがコードの一部を最適化するのに頼ることはできません。コンパイラは、より大きな影響を与える他の何かを最適化することを選択する可能性があるからです。確実に知る唯一の方法は、システム上コンパイラで両方のアプローチを試して、何が起こるかを確認することです。プロファイラーの結果を確認してください。の呼び出しが突出している場合は、別の変数に保存します。それ以外の場合は、心配しないでください。.end()

于 2013-03-15T13:20:12.960 に答える
2

vectorのようなコンテナは変数を返します。変数は、end()呼び出し時に、最適化された最後へのポインタを格納します。end()呼び出し時にルックアップなどを行うコンテナを作成した場合は、作成を検討してください

for (list<Employee>::const_iterator i = emps.begin(), end = emps.end(); i != end; ++i)
{
...
}

スピードのために

于 2013-03-15T13:15:49.667 に答える
0

アルゴリズムを使用するstd

もちろん彼は正しい。呼び出しendは一時オブジェクトをインスタンス化して破棄する可能性がありますが、これは一般的に悪いことです。

もちろん、コンパイラーは多くの場合これを最適化することができます。

より優れた、より堅牢なソリューションがあります。ループをカプセル化します

あなたが与えた例は、実際std::findには、戻り値を与えるか取るかです。他の多くのループにもstdアルゴリズム、または少なくとも適応できるほど類似したものがありますtransform_if。たとえば、私のユーティリティライブラリには実装があります。

したがって、関数内のループを非表示にして、にを取りconst&ますend。例と同じ修正ですが、はるかにクリーンです。

于 2013-03-15T13:25:43.050 に答える