C++は循環内と循環外で変数の違いを定義します.

28165 ワード

この文章を書く理由は、問答プラットフォームで見た問題です.
C++の内側の循環における変数の定義と外での定義の比率は大きな影響がありますか?
問題の由来:http://ask.csdn.net/questions/176270
例えば、for(int i=0;i<999;i+){for(int j=0;j<999;j+);
内層サイクルは、jがどれぐらい消費されるかを毎回定義していますか?
ここで私が答えたのは、
これはあなたが具体的にどんなコンパイラを使うかを見る必要があります.しかし、主流コンパイラ(vsやgccなど)の最適化は良く、変数を繰り返し割り当てません.
答えとコメントを見て、多くの人が興味を持っているようですので、実測を共有してみたいと思います.次のコードを書いてテストしました.
#include 
using namespace std;

void Test1()
{
    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("%d,%d
"
, int(i), int(j)); } } } void Test2() { int i, j; for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { printf("%d,%d
"
, int(i), int(j)); } } } int main() { Test1(); Test2(); return 0; }
OKです.プログラムは非常に簡単です.Test1Test2は2つのループです.同じことをすると、2重ループでijの値を印刷します.違いはサイクル外で変数jを定義し、もう1つはサイクル内で変数jを定義します.
ここでg++を使ってコンパイルします.最適化レベルはO0です.
g++ -O0 -g test.cpp
コンパイルした後、私は生成したTest 1関数とTest 2関数を逆アセンブルしました.GCC関数の逆アセンブリは以下の通りです.
(gdb) disas /m Test1
Dump of assembler code for function Test1():
5       {
   0x0804841d 0>:     push   %ebp
   0x0804841e 1>:     mov    %esp,%ebp
   0x08048420 3>:     sub    $0x28,%esp

6           for (int i = 0; i < 2; i++)
   0x08048423 6>:     movl   $0x0,-0x10(%ebp)
   0x0804842a 13>:    jmp    0x804845d 64>
   0x08048459 60>:    addl   $0x1,-0x10(%ebp)
   0x0804845d 64>:    cmpl   $0x1,-0x10(%ebp)
   0x08048461 68>:    jle    0x804842c 15>

7           {
8               for (int j = 0; j < 3; j++)
   0x0804842c 15>:    movl   $0x0,-0xc(%ebp)
   0x08048433 22>:    jmp    0x8048453 54>
   0x0804844f 50>:    addl   $0x1,-0xc(%ebp)
   0x08048453 54>:    cmpl   $0x2,-0xc(%ebp)
   0x08048457 58>:    jle    0x8048435 24>

9               {
10                  printf("%d,%d
"
, int(i), int(j)); 0x08048435 24>: mov -0xc(%ebp),%eax 0x08048438 27>: mov %eax,0x8(%esp) 0x0804843c 31>: mov -0x10(%ebp),%eax 0x0804843f 34>: mov %eax,0x4(%esp) 0x08048443 38>: movl $0x8048560,(%esp) 0x0804844a 45>: call 0x80482f0 <printf@plt> 11 } 12 } 13 14 } 0x08048463 70>: leave 0x08048464 71>: ret
Test1関数の逆アセンブリは以下の通りです.
(gdb) disas /m Test2
Dump of assembler code for function Test2():
17      {
   0x08048465 0>:     push   %ebp
   0x08048466 1>:     mov    %esp,%ebp
   0x08048468 3>:     sub    $0x28,%esp

18          int i, j;
19
20          for (i = 0; i < 2; i++)
   0x0804846b 6>:     movl   $0x0,-0x10(%ebp)
   0x08048472 13>:    jmp    0x80484a5 64>
   0x080484a1 60>:    addl   $0x1,-0x10(%ebp)
   0x080484a5 64>:    cmpl   $0x1,-0x10(%ebp)
   0x080484a9 68>:    jle    0x8048474 15>

21          {
22              for (j = 0; j < 3; j++)
   0x08048474 15>:    movl   $0x0,-0xc(%ebp)
   0x0804847b 22>:    jmp    0x804849b 54>
   0x08048497 50>:    addl   $0x1,-0xc(%ebp)
   0x0804849b 54>:    cmpl   $0x2,-0xc(%ebp)
   0x0804849f 58>:    jle    0x804847d 24>

23              {
24                  printf("%d,%d
"
, int(i), int(j)); 0x0804847d 24>: mov -0xc(%ebp),%eax 0x08048480 27>: mov %eax,0x8(%esp) 0x08048484 31>: mov -0x10(%ebp),%eax 0x08048487 34>: mov %eax,0x4(%esp) 0x0804848b 38>: movl $0x8048560,(%esp) 0x08048492 45>: call 0x80482f0 <printf@plt> 25 } 26 } 27 } 0x080484ab 70>: leave 0x080484ac 71>: ret End of assembler dump.
Test2の反則アセンブルでは、内部Test1の下に、分配変数for (int j = 0; j < 3; j++)のアセンブル命令が見えませんでした.もしjTest1のアセンブルコードだけを印刷したら、比較してみます.この2つの関数から発生するアセンブル命令は全く同じです.
(gdb) disas Test1
Dump of assembler code for function Test1():
   0x0804841d 0>:     push   %ebp
   0x0804841e 1>:     mov    %esp,%ebp
   0x08048420 3>:     sub    $0x28,%esp
   0x08048423 6>:     movl   $0x0,-0x10(%ebp)
   0x0804842a 13>:    jmp    0x804845d 64>
   0x0804842c 15>:    movl   $0x0,-0xc(%ebp)
   0x08048433 22>:    jmp    0x8048453 54>
   0x08048435 24>:    mov    -0xc(%ebp),%eax
   0x08048438 27>:    mov    %eax,0x8(%esp)
   0x0804843c 31>:    mov    -0x10(%ebp),%eax
   0x0804843f 34>:    mov    %eax,0x4(%esp)
   0x08048443 38>:    movl   $0x8048560,(%esp)
   0x0804844a 45>:    call   0x80482f0 @plt>
   0x0804844f 50>:    addl   $0x1,-0xc(%ebp)
   0x08048453 54>:    cmpl   $0x2,-0xc(%ebp)
   0x08048457 58>:    jle    0x8048435 24>
   0x08048459 60>:    addl   $0x1,-0x10(%ebp)
   0x0804845d 64>:    cmpl   $0x1,-0x10(%ebp)
   0x08048461 68>:    jle    0x804842c 15>
   0x08048463 70>:    leave  
   0x08048464 71>:    ret    
End of assembler dump.
(gdb) disas Test2   
Dump of assembler code for function Test2():
   0x08048465 0>:     push   %ebp
   0x08048466 1>:     mov    %esp,%ebp
   0x08048468 3>:     sub    $0x28,%esp
   0x0804846b 6>:     movl   $0x0,-0x10(%ebp)
   0x08048472 13>:    jmp    0x80484a5 64>
   0x08048474 15>:    movl   $0x0,-0xc(%ebp)
   0x0804847b 22>:    jmp    0x804849b 54>
   0x0804847d 24>:    mov    -0xc(%ebp),%eax
   0x08048480 27>:    mov    %eax,0x8(%esp)
   0x08048484 31>:    mov    -0x10(%ebp),%eax
   0x08048487 34>:    mov    %eax,0x4(%esp)
   0x0804848b 38>:    movl   $0x8048560,(%esp)
   0x08048492 45>:    call   0x80482f0 @plt>
   0x08048497 50>:    addl   $0x1,-0xc(%ebp)
   0x0804849b 54>:    cmpl   $0x2,-0xc(%ebp)
   0x0804849f 58>:    jle    0x804847d 24>
   0x080484a1 60>:    addl   $0x1,-0x10(%ebp)
   0x080484a5 64>:    cmpl   $0x1,-0x10(%ebp)
   0x080484a9 68>:    jle    0x8048474 15>
   0x080484ab 70>:    leave  
   0x080484ac 71>:    ret    
End of assembler dump.
もちろん、ここではTest2のコンパイル効果だけをテストしました.g++の下の効果はみんな自分でテストできます.vsのコンパイラを使ったら、循環外で変数を定義しますか?それとも循環内で変数を定義しますか?効果は全く同じです.でも、コードの美しさのために循環内に書きましょう.
上記では、基本的なデータタイプgccを循環変数として使用する場合について検討しましたが、ここではステップを進める必要があります.私が使っているのはintではなく、複雑なオブジェクトの場合、ループの効果はどうなりますか?
変数の割り当てを見やすくするために、クラスのコンストラクターリガで文言を印刷しました.クラスのオブジェクトが作成された状況を簡単に見られます.
#include 
using namespace std;

class MyInt
{
public:
    MyInt(int i):
        m_iValue(i)
    {
        printf("Constructed: MyInt(%d)
"
, i); } MyInt() { printf("Constructed: MyInt()
"
); } MyInt &operator++(int i) { m_iValue ++; return *this; } bool const operator const MyInt& another) { return m_iValue < another.m_iValue; } operator int() { return m_iValue; } MyInt &operator =(int i) { m_iValue = i; return *this; } private: int m_iValue; }; void Test1() { for (MyInt i = MyInt(0); i < MyInt(2); i++) { for (MyInt j = MyInt(0); j < MyInt(3); j++) { printf("%d,%d
"
, int(i), int(j)); } } } void Test2() { MyInt i, j; for (i = MyInt(0); i < MyInt(2); i++) { for (j = MyInt(0); j < MyInt(3); j++) { printf("%d,%d
"
, int(i), int(j)); } } } void Test3() { MyInt i, j; for (i = 0; int(i) < 2; i++) { for (j = 0; int(j) < 3; j++) { printf("%d,%d
"
, int(i), int(j)); } } } int main() { printf("Test1---------------------------------
"
); Test1(); printf("Test2---------------------------------
"
); Test2(); printf("Test3---------------------------------
"
); Test3(); return 0; }
はい、やはりg++-O 0を使ってコンパイルします.実行結果を見てみます.
Test1---------------------------------
Constructed: MyInt(0)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
0,0
Constructed: MyInt(3)
0,1
Constructed: MyInt(3)
0,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
1,0
Constructed: MyInt(3)
1,1
Constructed: MyInt(3)
1,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Test2---------------------------------
Constructed: MyInt()
Constructed: MyInt()
Constructed: MyInt(0)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
0,0
Constructed: MyInt(3)
0,1
Constructed: MyInt(3)
0,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Constructed: MyInt(0)
Constructed: MyInt(3)
1,0
Constructed: MyInt(3)
1,1
Constructed: MyInt(3)
1,2
Constructed: MyInt(3)
Constructed: MyInt(2)
Test3---------------------------------
Constructed: MyInt()
Constructed: MyInt()
0,0
0,1
0,2
1,0
1,1
1,2
intは、オブジェクトを作成する回数が最も少なく、オブジェクトが複雑であれば、Test 3が最も効率的な符号化方式であることは明らかである.
プログラム全体の出力について、分析してみます.
  • C+に内蔵された基本データタイプに対して、コンパイラは相関最適化があり、二重循環では対象の繰り返し割り当てが回避されますが、複雑なクラスオブジェクトに対しては、コンパイラは簡単に最適化されないようです.
  • は、Test3において、私たちは循環外でj変数を定義しているので、ここではj変数に対する繰り返しの割り当ては発生していませんが、割当条件Test1Test2と判断条件i = MyInt(0)j = MyInt(0)オブジェクトを構成する必要があるので、巡回中の複数の変数の割り当てを依然として見ています.
  • i < MyInt(2)では、j < MyInt(3)を用いて直接に対象に整数値を付与し、賦値文にMyInt(2)オブジェクトを作成し、MyInt(3)Test3を用いて判断条件に =オブジェクトを作成することを避けたので、セグメントコード全体には循環外に2回の変数のみが割り当てられている.これは実は一番効率的な方法です.
  • 最後のまとめ:
  • は、intなどの基本的なデータタイプを循環変数として使用する場合、あなたが使用する最適化の面で十分な力を与える主流のコンパイラであれば、循環外または循環内で循環変数を定義することに全く関心を持たないでください.
  • 循環変数自体が複雑なオブジェクトである場合は、ループ外で定義されることを推奨し、MyIntループの割当文、判断文では、オブジェクトの作成を繰り返すことを避けるべきである.