程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 優化C++代碼(4):消除冗余代碼

優化C++代碼(4):消除冗余代碼

編輯:C++入門知識

這篇文章講述了消除冗余代碼Dead Code Elimination)的優化方法,我簡寫為DCE。顧名思義:只要其計算結果沒有被程序所使用, 該計算就被丟棄。

這時你可能會說,你的代碼只會計算有用的結果,從沒有無用的東西,只有白癡才會平白無故地添加那些無用的代碼—–例如,會在做一些有用事情的同時,還在計算著圓周率的前1000位。那麼消除冗余代碼的優化,到底什麼時候才有用呢?

我之所以這麼早就開始講述DCE的優化,是因為如果不清楚DCE的話,在探索其他一些更有趣的優化方法中會造成一些破壞和混亂。看一下下面的小例子,文件Sum.cpp:

  1. int main() { 
  2.     long long s = 0; 
  3.     for (long long i = 1; i <= 1000000000; ++i) s += i; 
  4. }

我們對於在計算這十億個數的和時,循環的執行速度很感興趣。的確,這種做法太愚蠢了,我們高中時就學過有一個閉公式可以計算出結果,但這不是重點)

使用命令 CL /Od /FA Sum.cpp 來 構建這個程序,並用Sum命令來運行程序。注意這個構建使用/Od開關禁用了代碼優化。 在我的PC機上,運行這個程序花費了4秒。 現在試著使用CL /O2 /FA Sum.cpp 來編譯優化過的代碼。這次運行很快,幾乎察覺不到有延遲。編譯器對我們的代碼的優化有這麼出色嗎?答案是否定的但它的確是以一種奇怪的方式改變了我們的 代碼)

我們看一下/Od版本生成的代碼,它保存在Sum.asm裡。我精減了一些代碼並注釋了一些文本,讓它只顯示循環體:

這些指令跟你預料中的差不多。 變量i保存以在RSP為寄存器,i$1為偏移量的棧上;在asm文件的其他地方,我們發現i$1=0.  使用RAX寄存器讓i自增長。同樣地,變量s保存在RSP為寄存器,S$為偏移量的棧上,s$=8.  並在RCX中來計算每次循環的累積和。

我們注意到每次循環時,先從棧上獲取i的值,再把新值寫回去,變量s同樣如此。可以說這段代碼很幼稚—–它是由很愚蠢的編譯器生成的也就說,優化被禁用了)。 例如,我們本來可以將變量i和s一直保存在寄存器中,而不用每次循環迭代時都要訪問內存。

關於未優化的代碼就說這麼多了。那麼進行優化後所生成代碼是什麼樣呢? 來看一下使用/O2構建的程序對應的Sum.asm文件,再一次把文件精簡到只剩下循環體的實現,

結果是:

  1. ;; there’s nothing here! 

對,它是空的,沒有任何計算s的指令。

你可能會說,那這個答案肯定錯了.但是我們怎麼知道這個答案就是錯的呢?因為優化器已經推斷出程序在任何時候都沒用到S, 所以才懶得計算它。你不能說答案是錯的,除非你需要核對該答案,對吧?

我們這不是被DCE優化給耍了嗎? 如果你不需要觀察計算結果,程序是不會進行計算的。

優化器的這個問題其實跟量子物理學的基本原理很類似,可以用大眾科普文章裡經常提到的一句話來解釋,“如果森林裡一棵樹倒下了,但是如果周圍都沒人,它還會有聲音嗎?”。

我們可以在代碼裡添加打印變量s的語句來觀察計算結果,代碼如下:

  1. #include <stdio.h> 
  2. int main() { 
  3.     long long s = 0; 
  4.     for (long long i = 1; i <= 1000000000; ++i) s += i; 
  5.     printf("%lld ", s); 

運行/Od版本的程序打印出了正確結果,還是花費了4秒,/O2版本打印出同樣的結果,速度卻快得多具體快多少,可以看下下面的可選部分,實際上,速度高達7倍多)。

到現在為止,我已經講述了這篇文章的主要觀點:在進行編譯器優化分析的時候一定要十分小心,衡量它們的優點時,千萬不要被DCE給誤導了。 下面是使用DCE優化時需要注意的四個步驟:

不管怎麼樣,我們從這個例子中還是學到了一些很有意思的東西,下面四個小節為可選部分。

  1. xor    edx, edx 
  2. mov    eax, 1 
  3. mov    ecx, edx 
  4. mov    r8d, edx 
  5. mov    r9d, edx 
  6. npad   13 
  7. $LL3@main: 
  8. inc    r9 
  9. add    r8, 2 
  10. add    rcx, 3 
  11. add    r9, rax                           ;; r9  = 2  8 18 32 50 ... 
  12. add    r8, rax                           ;; r8  = 3 10 21 36 55 ... 
  13. add    rcx, rax                          ;; rcx = 4 12 24 40 60 ... 
  14. add    rdx, rax                          ;; rdx = 1  6 15 28 45 ... 
  15. add    rax, 4                            ;; rax = 1  5  9 13 17 ... 
  16. cmp    rax, 1000000000                   ;; i <= 1000000000 ? 
  17. jle    SHORT $LL3@main                   ;; yes, so loop back 

注意看循環體包含了和未優化版本一樣多的指令,為什麼會快很多呢?那是因為優化後的循環體的指令使用的是寄存器,而不是內存地址。 我們都知道,寄存器的訪問速度比內存快得多。 下面的延遲就展示了內存訪問時如何將你的程序降到蝸牛般的速度:

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved