歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C++在循環內和循環外定義變量的差異(如何寫出高效的for循環)

C++在循環內和循環外定義變量的差異(如何寫出高效的for循環)

日期:2017/3/1 9:29:34   编辑:Linux編程

寫這篇文章的原因是我在問答平台看到的一個問題:C++內層循環中定義變量和在外面定義比影響大嗎?

例如:
for(int i=0;i<999;i++) {
for(int j=0;j<999;j++);
}

內層循環每次都定義j會造成多大的消耗呢?

此處我給出的回答是:

這個需要看你具體用什麼編譯器。不過主流編譯器(如vs和gcc)這一塊優化都比較好,不會反復分配變量。

看到答案和評論,好像有很多人對這個感興趣,所以我打算給大家實測分享一下,於是寫了如下代碼進行測試:

#include <cstdio>
using namespace std;

void Test1()
{
    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("%d,%d\n", int(i), int(j));
        }
    }

}

void Test2()
{
    int i, j;

    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 3; j++)
        {
            printf("%d,%d\n", int(i), int(j));
        }
    }
}

int main()
{
    Test1();
    Test2();

    return 0;
}

OK,程序非常簡單,Test1Test2是兩個循環,干相同的事情,就是在雙重循環裡打印一下 ij 的值,差別只在於一個在循環外定義變量 j,另一個在循環內定義變量 j

此處我使用g++進行編譯,優化等級是O0(這是GCC默認的優化等級,也是最低的優化等級)的:

g++ -O0 -g test.cpp

編譯後,我將生成的Test1函數和Test2函數反匯編出來,得出的結果是這樣的:

Test1函數反匯編如下:

(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 <Test1()+64>
   0x08048459 <+60>:    addl   $0x1,-0x10(%ebp)
   0x0804845d <+64>:    cmpl   $0x1,-0x10(%ebp)
   0x08048461 <+68>:    jle    0x804842c <Test1()+15>

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

9               {
10                  printf("%d,%d\n", 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    

Test2函數反匯編如下:

(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 <Test2()+64>
   0x080484a1 <+60>:    addl   $0x1,-0x10(%ebp)
   0x080484a5 <+64>:    cmpl   $0x1,-0x10(%ebp)
   0x080484a9 <+68>:    jle    0x8048474 <Test2()+15>

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

23              {
24                  printf("%d,%d\n", 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.

Test1的反匯編中,我們在內部for (int j = 0; j < 3; j++)下面,沒有看到分配變量 j 的匯編指令,如果再只打印Test1Test2的匯編代碼,經過對比,你們發現這兩個函數產生的匯編指令是完全一樣的:

(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 <Test1()+64>
   0x0804842c <+15>:    movl   $0x0,-0xc(%ebp)
   0x08048433 <+22>:    jmp    0x8048453 <Test1()+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 <printf@plt>
   0x0804844f <+50>:    addl   $0x1,-0xc(%ebp)
   0x08048453 <+54>:    cmpl   $0x2,-0xc(%ebp)
   0x08048457 <+58>:    jle    0x8048435 <Test1()+24>
   0x08048459 <+60>:    addl   $0x1,-0x10(%ebp)
   0x0804845d <+64>:    cmpl   $0x1,-0x10(%ebp)
   0x08048461 <+68>:    jle    0x804842c <Test1()+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 <Test2()+64> 0x08048474 <+15>: movl $0x0,-0xc(%ebp) 0x0804847b <+22>: jmp 0x804849b <Test2()+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 <printf@plt> 0x08048497 <+50>: addl $0x1,-0xc(%ebp) 0x0804849b <+54>: cmpl $0x2,-0xc(%ebp) 0x0804849f <+58>: jle 0x804847d <Test2()+24> 0x080484a1 <+60>: addl $0x1,-0x10(%ebp) 0x080484a5 <+64>: cmpl $0x1,-0x10(%ebp) 0x080484a9 <+68>: jle 0x8048474 <Test2()+15> 0x080484ab <+70>: leave 0x080484ac <+71>: ret End of assembler dump.

當然,這裡只測試了g++的編譯效果。vs下的效果大家可以自己測試。目前可以肯定,如果你使用gcc的編譯器,你完全可以不用糾結在循環外定義變量還是循環內定義變量,因為效果完全是一樣的,不過為了代碼好看,還是寫到循環內吧。

上面已經探究了使用基本數據類型int作為循環變量的情況,這裡需要進階一下,探討一下如果我使用的不是int,而是一個復雜的對象,那循環的效果又是如何呢?

為了方便看到變量的分配,我在類的構造函數裡加了打印語句,可以讓我們方便地看到類的對象被創建的情況:

#include <cstdio>
using namespace std;

class MyInt
{
public:
    MyInt(int i):
        m_iValue(i)
    {
        printf("Constructed: MyInt(%d)\n", i);
    }

    MyInt()
    {
        printf("Constructed: MyInt()\n");
    }

    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\n", 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\n", 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\n", int(i), int(j));
        }
    }
}


int main()
{
    printf("Test1---------------------------------\n");
    Test1();

    printf("Test2---------------------------------\n");
    Test2();

    printf("Test3---------------------------------\n");
    Test3();

    return 0;
}

好的,還是使用g++ -O0編譯,我們來看看執行結果:

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

可以看到,Test3創建對象的次數是最少的,如果對象比較復雜,顯然Test3會是最高效的編碼方式。

對於整個程序的輸出,我們可以分析一下:

  • 對於C++內置的基本數據類型,編譯器有相關的優化,在雙重循環中會避免掉對象的反復分配,但對於復雜的類對象,編譯器似乎不會輕易優化,所以我們在Test1中仍然看到了對j變量多次分配動作。
  • Test2中,由於我們在循環外定義了j變量,所以這裡沒有發生對j變量的反復分配,但由於賦值條件i = MyInt(0)j = MyInt(0)以及判斷條件i < MyInt(2)j < MyInt(3)中需要構造MyInt(2)MyInt(3)對象,所以我們仍然看到循環中多次的變量分配。
  • 而在Test3中,我們換了一種方式,用重載運算符=直接在賦值語句中給對象賦整型值,避免了賦值語句中創建MyInt對象,並用int(i) < 2int(j) < 3,避免了在判斷條件裡創建MyInt對象,所以整段代碼裡只在循環外分配了兩次變量,這其實是最高效的方式。

最後總結:

  1. 對於使用int等基本數據類型作為循環變量,只要你用的優化方面足夠給力的主流的編譯器,完全不需要關心在循環外還是循環內定義循環變量。
  2. 如果循環變量本身是復雜的對象,建議在循環外定義好,並且在for循環的賦值語句、判斷語句中,都要避免重復創建對象。

------------------------------分割線------------------------------

C++ Primer Plus 第6版 中文版 清晰有書簽PDF+源代碼 http://www.linuxidc.com/Linux/2014-05/101227.htm

讀C++ Primer 之構造函數陷阱 http://www.linuxidc.com/Linux/2011-08/40176.htm

讀C++ Primer 之智能指針 http://www.linuxidc.com/Linux/2011-08/40177.htm

讀C++ Primer 之句柄類 http://www.linuxidc.com/Linux/2011-08/40175.htm

將C語言梳理一下,分布在以下10個章節中:

  1. Linux-C成長之路(一):Linux下C編程概要 http://www.linuxidc.com/Linux/2014-05/101242.htm
  2. Linux-C成長之路(二):基本數據類型 http://www.linuxidc.com/Linux/2014-05/101242p2.htm
  3. Linux-C成長之路(三):基本IO函數操作 http://www.linuxidc.com/Linux/2014-05/101242p3.htm
  4. Linux-C成長之路(四):運算符 http://www.linuxidc.com/Linux/2014-05/101242p4.htm
  5. Linux-C成長之路(五):控制流 http://www.linuxidc.com/Linux/2014-05/101242p5.htm
  6. Linux-C成長之路(六):函數要義 http://www.linuxidc.com/Linux/2014-05/101242p6.htm
  7. Linux-C成長之路(七):數組與指針 http://www.linuxidc.com/Linux/2014-05/101242p7.htm
  8. Linux-C成長之路(八):存儲類,動態內存 http://www.linuxidc.com/Linux/2014-05/101242p8.htm
  9. Linux-C成長之路(九):復合數據類型 http://www.linuxidc.com/Linux/2014-05/101242p9.htm
  10. Linux-C成長之路(十):其他高級議題

Copyright © Linux教程網 All Rights Reserved