愛悠閑 > C語言深度解剖之——編譯器的bug、for循環優化

C語言深度解剖之——編譯器的bug、for循環優化

分類: 面試相關  |  標簽: 編譯器,c,優化,語言,null,存儲  |  作者: chen825919148 相關  |  發布日期 : 2014-06-10  |  熱度 : 249°

C語言深度解剖》的作者是個善于觀察、思維縝密的人,在其著作中提出了許多值得思考的問題和細節,對于理解計算機系統原理具有很好的參考價值。這兩天拜讀了此書,今天跟大家一起探討一下書中一個關于指針的有趣現象。如果你尚未讀過原文,請先閱讀原書對應的如下章節:

***********************************以下是原文**************************************

4.1.5,編譯器的bug

另外一個有意思的現象,在Visual C++ 6.0 調試如下代碼的時候卻又發現一個古怪的問題:

int *p = (int *)0x12ff7c;

*p = NULL;

p = NULL;

在執行完第二條代碼之后,發現p 的值變為0x00000000 了。按照我么上一節的解釋,應該p的值不變,只是p 指向的內存被賦值為0。難道我們講錯了嗎?別急,再試試如下代碼:

int i = 10;

int *p = (int *)0x12ff7c;

*p = NULL;

p = NULL;

通過調試,發現這樣子的話,p 的值沒有變,而p 指向的內存的值變為0 了。這與我們前面講解的完全一致。當然這里的i 的地址剛好是0x12ff7c,但這并不能改變“*p = NULL;”這行代碼的功能。

為了再次測試這個問題,我又調試了如下代碼:

int i = 10;

int j = 100;

int *p = (int *)0x12ff78;

*p = NULL;

p = NULL;

這里0x12ff78 剛好就是變量j 的地址。這樣的話一切正常,但是如果把“int j = 100;”這行代碼刪除的話,又出現上述的問題了。測試到這里我還是不甘心,編譯器怎么能犯這種低級錯誤呢?于是又接著進行了如下測試:

unsigned int i = 10;

//unsigned int j = 100;

unsigned int *p = (unsigned int *)0x12ff78;

*p = NULL;

p = NULL;

得到的結果與上面完全一樣。當然,我還是沒有死心,又進行了如下測試:

char ch = 10;

char *p = (char *)0x12ff7c;

*p = NULL;

p = NULL;

這樣子的話,完全正常。但當我刪除掉第一行代碼后再測試,這里的p的值并未變成0x00000000,而是變成了0x0012ff00,同時*p 的值變成了0。這又是怎么回事呢?初學者是否認為這是編譯器良心發現,把*p 的值改寫為0 了。

如果你真這么認為,那就大錯特錯了。這里的*p 還是地址0x12ff7c 上的內容嗎?顯然不是,而是地址0x0012ff00 上的內容。至于0x12ff7c 為什么變成0x0012ff00,則是因為編譯器認為這是把NULL 賦值給char 類型的內存,所以只是把指針變量p 的低地址上的一個字節賦值為0。至于為什么是低地址,請參看前面講解過大小端模式相關內容。

測試到這里,已經基本可以肯定這是Visual C++ 6.0 的一個bug。所以平時一定不要迷信某個編譯器,要相信自己的判斷。當然,后面還會提到一個我認為的Visual C++ 6.0 的一個bug。還有,這個小小的例子,你是否可以在多個編譯器上測試測試呢?

************************************以上是原文*************************************

到此,相信你對作者所發現的有趣現象已經有所了解,現在就讓我們通過實驗+觀察+分析,給這一現象一個合理的解釋,一起來探討一下,為什么會產生這種現象,這究竟是否是VC 6.0編譯器存在的bug

首先看作者給出的第一個例子:

int *p = (int *)0x12ff7c;

*p = NULL;

p = NULL;

注意0x12ff7c的由來。作者是通過在變量p之前先定義變量i,然后在調試模式下觀察到i的存儲地址為0x12ff7c,接著將變量i的定義去掉,令p指向地址0x12ff7c對應的存儲單元。不同的系統此值一般不同,這取決于操作系統為進程分配的內存空間(主要是堆棧區)的不同,請大家自行替換成合適的值。在我的計算機系統上調試時,對應的地址是0x0013ff7c

接下來請大家測試如下代碼,并對比四條printf語句的輸出:

       int *p;

       printf("&p=%x,p=%x\n",&p,p);

       p = (int *)0x0013ff7c;

       printf("&p=%x,p=%x\n",&p,p);

       *p = NULL;

       printf("&p=%x,p=%x\n",&p,p);

       p = NULL;

       printf("&p=%x,p=%x\n",&p,p);

以下是輸出結果:

&p=13ff7c,p=cccccccc

&p=13ff7c,p=13ff7c

&p=13ff7c,p=0

&p=13ff7c,p=0

通過觀察我們可以發現,變量p自定義以來,其存儲位置(&p)并未改變,始終是0x13ff7c,隨著對p*p的操作發生變化的是p值。到此,大家已經隱約覺得這未必是編譯器的bug了吧?繼續分析!大家一定注意到了:&p恰好也是0x13ff7c,與p所指向的地址一致,這正是關鍵所在!語句p = (int *)0x0013ff7c使得p恰好指向其本身(這一點作者可能并未意識到,其實這很正常,分給該進程的堆棧空間起始位置固定,i原本在p之前緊挨著p定義,現將其去掉,自然原本應分給i的位置便分配給了變量p,而且int型和指針型都占4個字節的存儲空間)。理解此問題的關鍵在于正確區別&pp&p表示系統為指針變量p分配的存儲空間的位置,而變量窗口中顯示的p值,代表為變量p所分配的存儲單元中存儲的內容,即它所指向的變量的地址。作者通過*p=NULLp所指向的內容改為0,不就是將p值改成0了么?

由此可見,此現象并非編譯器有bug,而是作者不慎混淆了&pp的含義!

再看作者最后舉的一個例子,我仍將相應的地址改為0x13ff7c

char *p = (char *)0x13ff7c; //0x13ff7c原是字符型變量ch對應的地址,現為變量p的首地址

*p = NULL;

p = NULL;

作者正確觀察到,執行完*p = NULL后,p的值并未變成0x00000000,而是變成了0x0013ff00,同時*p 的值變成了0。這究竟是怎么回事呢?

這里,指針p依然是指向自身,但此時的p為字符型指針,*p代表的僅為4字節變量p起始存儲地址對應的字節,對于x86體系結構(little-endian)來說,是變量p內容的最低字節。執行完*p = NULL后,p內容的最低字節變為0(即*p變為0),即原來的最低字節0x7c被換成了0x00(注意此前p的內容為0x0013ff7c),于是p的內容變為0x0013ff00,即此時p已經指向新的地址0x0013ff00

不習慣在debug模式下觀察變量值的讀者可以用下面的代碼去觀察輸出結果:

       char *p;         //此時已為變量p分配空間,但未初始化,其指向不定

       printf("&p=%x,p=%x\n",&p,p); 

       p = (char *)0x0013ff7c;

       printf("&p=%x,p=%x\n",&p,p);

       *p = NULL;           //p的最低字節被改為0x00

       printf("&p=%x,p=%x\n",&p,p);

       p = NULL;            //p值(4字節)被置零

       printf("&p=%x,p=%x\n",&p,p);

輸出結果為:

&p=13ff7c,p=cccccccc

&p=13ff7c,p=13ff7c

&p=13ff7c,p=13ff00

&p=13ff7c,p=0

 

通過上述實驗與分析,我們可以發現,理論分析與實驗結果完全吻合,編譯器工作正常,并不存在什么所謂的bug

 

歡迎大家發現問題,共同探討,加深理解,天天進步![email protected]

 

 

忽然又想起一個問題,1.8.2有關循環優化:

************************************以下是原文************************************

1.8.2,循環語句的注意點

【建議1-27】在多重循環中,如果有可能,應當將最長的循環放在最內層,最短的循環放在最外層,以減少CPU 跨切循環層的次數。

例如:

************************************以上是原文************************************

其實,右邊的循環之所以比左邊的效率高,本質原因并非是循環長短的問題,而是與程序訪問的局部性和Cache命中率有關。計算機專業畢業的學生應該很清楚這個問題,在《操作系統》和《體系結構》課程中一般都會探討此問題。我們知道,數組在計算機中是行優先存儲的(即本行的最后一個元素與下一行的第一個元素地址相鄰),左邊的循環中,依次訪問的是變量a[0][0],a[1][0],a[2][0],……,a[99][0],a[0][1],a[1][1],a[2][1],……,a[99][1],……這實際上是按照列優先的原則在訪問數組元素。如果Cache容量相對于數組容量而言不夠大,考慮一個極端情況,假設Cache只有一個塊,只能存儲一行數據,則每訪問一個元素就會發生一次Cache失效,就需要訪問一次主存,讀入一塊數據,導致存儲系統效率低下,明顯影響操作延遲。而右邊的循環采用的是行優先訪問原則,與元素存儲順序一致。基于同樣的假設,此時只有訪問新一行的第一個數據時才發生Cache失效,通過訪問主存讀入一塊連續的數據(恰為數組的一行),此后訪問同行數據便可直接使用Cache中緩存的數據,直到訪問下一行的第一個數據。Cache失效率降低了,整個存儲系統的平均訪問延遲降低了,顯然程序執行效率較高。

內外循環交換是優化程序性能的重要手段之一,右邊程序的存儲訪問局部性較好,建議如此編程。



快乐彩中奖说明