• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆 - 47, 文章 - 10, 評論 - 8, 引用 - 0
            數(shù)據(jù)加載中……

            簡明x86匯編語言教程(七)

            原創(chuàng):司徒彥南

            5.0 編譯優(yōu)化概述

            優(yōu)化是一件非常重要的事情。作為一個(gè)程序設(shè)計(jì)者,你肯定希望自己的程序既小又快。DOS時(shí)代的許多書中都提到,“某某編譯器能夠生成非常緊湊的代碼”,換言之,編譯器會為你把代碼盡可能地縮減,如果你能夠正確地使用它提供的功能的話。目前,Intel x86體系上流行的C/C++編譯器,包括Intel C/C++ Compiler, GNU C/C++ Compiler,以及最新的Microsoft和Borland編譯器,都能夠提供非常緊湊的代碼。正確地使用這些編譯器,則可以得到性能足夠好的代碼。

            但是,機(jī)器目前還不能像人那樣做富于創(chuàng)造性的事情。因而,有些時(shí)候我們可能會不得不手工來做一些事情。

            使用匯編語言優(yōu)化代碼是一件困難,而且技巧性很強(qiáng)的工作。很多編譯器能夠生成為處理器進(jìn)行過特殊優(yōu)化處理的代碼,一旦進(jìn)行修改,這些特殊優(yōu)化可能就會被破壞而失效。因此,在你決定使用自己的匯編代碼之前,一定要測試一下,到底是編譯器生成的那段代碼更好,還是你的更好。

            本章中將討論一些編譯器在某些時(shí)候會做的事情(從某種意義上說,本章內(nèi)容更像是計(jì)算機(jī)專業(yè)的基礎(chǔ)課中《編譯程序設(shè)計(jì)原理》、《計(jì)算機(jī)組成原理》、《計(jì)算機(jī)體系結(jié)構(gòu)》課程中的相關(guān)內(nèi)容)。本章的許多內(nèi)容和匯編語言程序設(shè)計(jì)本身關(guān)系并不是很緊密,它們多數(shù)是在為使用匯編語言進(jìn)行優(yōu)化做準(zhǔn)備。編譯器確實(shí)做這些優(yōu)化,但它并不總是這么做;此外,就編譯器的設(shè)計(jì)本質(zhì)來說,它確實(shí)沒有義務(wù)這么做——編譯器做的是等義變換,而不是等效變換。考慮下面的代碼:

            // 程序段1
            int
            gaussianSum(){
            ? int i, j=0;

            ? for(i=0; i<100; i++) j+=i;

            ? return j;
            }

            好的,首先,絕大多數(shù)編譯器恐怕不會自作主張地把它“篡改”為

            // 程序段1(改進(jìn)1)
            int gaussianSum(){
            ? int i, j=0;

            ? for(i=1; i<100; i++) j+=i;

            ? return j;
            }

            多數(shù)(但確實(shí)不是全部)編譯器也不會把它改為

            // 程序段1(改進(jìn)2)
            inline int gaussianSum(){
            ? return 5050;
            }

            這兩個(gè)修改版本都不同于原先程序的語義。首先我們看到,讓i從0開始是沒有必要的,因?yàn)閖+=i時(shí),i=0不會做任何有用的事情;然后是,實(shí)際上沒有必要每一次都計(jì)算1+...+100的和——它可以被預(yù)先計(jì)算,并在需要的時(shí)候返回。

            這個(gè)例子也許并不恰當(dāng)(估計(jì)沒人會寫出最初版本那樣的代碼),但這種實(shí)踐在程序設(shè)計(jì)中確實(shí)可能出現(xiàn)。我們把改進(jìn)2稱為編譯時(shí)表達(dá)式預(yù)先計(jì)算,而把改進(jìn)1成為循環(huán)強(qiáng)度削減

            然而,一些新的編譯器的確會進(jìn)行這兩種優(yōu)化。不過別慌,看看下面的代碼:

            // 程序段2
            int
            GetFactorial(int k){
            ? int i, j=1;

            ? if((k<0) || (k>=10)) return -1;

            ? if((k<=1)) return 1

            ? for(i=1; i<k; i++) j*=i;

            ? return j;
            }

            程序采用的是一個(gè)時(shí)間復(fù)雜度為O(n)的算法,不過,我們可以把他輕易地改為O(1)的算法:

            // 程序段2 (非規(guī)范改進(jìn))
            int
            GetFactorial(int k){
            ? int i, j=1;

            ? static const int FractorialTable[]={1, 1, 2, 6, 24,
            ??? 120, 720, 5040, 40320, 362880, 3628800};

            ? if((k<0) || (k>=10)) return -1;

            ? return FractorialTable[k];
            }

            這是一個(gè)典型的以空間換時(shí)間的做法。通用的編譯器不會這么做——因?yàn)樗鼪]有辦法在編譯時(shí)確定你是不是要這么改。可以說,如果編譯器真的這樣做的話,那將是一件可怕的事情,因?yàn)槟菚r(shí)候你將很難知道編譯器生成的代碼和自己想的到底有多大的差距。

            當(dāng)然,這類優(yōu)化超出了本文的范圍——基本上,我把它們歸入“算法優(yōu)化”,而不是“程序優(yōu)化”一類。類似的優(yōu)化過程需要程序設(shè)計(jì)人員對于程序邏輯非常深入地了解和全盤的掌握,同時(shí),也需要有豐富的算法知識。

            自然,如果你希望自己的程序性能有大幅度的提升,那么首先應(yīng)該做的是算法優(yōu)化。例如,把一個(gè)O(n2)的算法替換為一個(gè)O(n)的算法,則程序的性能提升將遠(yuǎn)遠(yuǎn)超過對于個(gè)別語句的修改。此外,一個(gè)已經(jīng)改寫為匯編語言的程序,如果要再在算法上作大幅度的修改,其工作量將和重寫相當(dāng)。因此,在決定使用匯編語言進(jìn)行優(yōu)化之前,必須首先考慮算法優(yōu)化。但假如已經(jīng)是最優(yōu)的算法,程序運(yùn)行速度還是不夠快怎么辦呢?

            好的,現(xiàn)在,假定你已經(jīng)使用了已知最好的算法,決定把它交給編譯器,讓我們來看看編譯器會為我們做什么,以及我們是否有機(jī)會插手此事,做得更好。

            5.1 循環(huán)優(yōu)化:強(qiáng)度削減和代碼外提

            比較新的編譯器在編譯時(shí)會自動(dòng)把下面的代碼:

            for(i=0; i<10; i++){
            ? j = i;
            ? k = j + i;
            }

            至少變換為

            for(i=0; i<10; i++);
            j=i; k=j+i;

            甚至

            j=i=10; k=20;

            當(dāng)然,真正的編譯器實(shí)際上是在中間代碼層次作這件事情。

            原理如果數(shù)據(jù)項(xiàng)的某個(gè)中間值(程序執(zhí)行過程中的計(jì)算結(jié)果)在使用之前被另一中間值覆蓋,則相關(guān)計(jì)算不必進(jìn)行。

            也許有人會問,編譯器不是都給咱們做了嗎,管它做什么?注意,這里說的只是編譯系統(tǒng)中優(yōu)化部分的基本設(shè)計(jì)。不僅在從源代碼到中間代碼的過程中存在優(yōu)化問題,而且編譯器生成的最終的機(jī)器語言(匯編)代碼同樣存在類似的問題。目前,幾乎所有的編譯器在最終生成代碼的過程中都有或多或少的瑕疵,這些瑕疵目前只能依靠手工修改代碼來解決。

            5.2 局部優(yōu)化:表達(dá)式預(yù)計(jì)算和子表達(dá)式提取

            表達(dá)式預(yù)先計(jì)算非常簡單,就是在編譯時(shí)盡可能地計(jì)算程序中需要計(jì)算的東西。例如,你可以毫不猶豫地寫出下面的代碼:

            const unsigned long nGiga = 1024L * 1024L * 1024L;

            而不必?fù)?dān)心程序每次執(zhí)行這個(gè)語句時(shí)作兩遍乘法,因?yàn)榫幾g器會自動(dòng)地把它改為

            const unsigned long nGiga = 1073741824L;

            而不是傻乎乎地讓計(jì)算機(jī)在執(zhí)行到這個(gè)初始化賦值語句的時(shí)候才計(jì)算。當(dāng)然,如果你愿意在上面的代碼中摻上一些變量的話,編譯器同樣會把常數(shù)部分先行計(jì)算,并拿到結(jié)果。

            表達(dá)式預(yù)計(jì)算并不會讓程序性能有飛躍性的提升,但確實(shí)減少了運(yùn)行時(shí)的計(jì)算強(qiáng)度。除此之外,絕大多數(shù)編譯器會把下面的代碼:

            // [假設(shè)此時(shí)b, c, d, e, f, g, h都有一個(gè)確定的非零整數(shù)值,并且,
            // a[]為一個(gè)包括5個(gè)整數(shù)元素的數(shù)組,其下標(biāo)為0到4]

            a[0] = b*c;
            a[1] = b+c;
            a[2] = d*e;
            a[3] = b*d + c*d;
            a[4] = b*d*e + c*d*e;?

            優(yōu)化為(再次強(qiáng)調(diào),編譯器實(shí)際上是在中間代碼的層次,而不是源代碼層次做這件事情!):

            // [假設(shè)此時(shí)b, c, d, e, f, g, h都有一個(gè)確定的非零整數(shù)值,并且,
            // a[]為一個(gè)包括5個(gè)整數(shù)元素的數(shù)組,其下標(biāo)為0到4]

            a[0] = b*c;
            a[1] = b+c;
            a[2] = d*e;
            a[3] = a[1] * d;
            a[4] = a[3] * e;

            更進(jìn)一步,在實(shí)際代碼生成過程中,一些編譯器還會對上述語句的次序進(jìn)行調(diào)整,以使其運(yùn)行效率更高。例如,將語句調(diào)整為下面的次序:

            // [假設(shè)此時(shí)b, c, d, e, f, g, h都有一個(gè)確定的非零整數(shù)值,并且,
            // a[]為一個(gè)包括5個(gè)整數(shù)元素的數(shù)組,其下標(biāo)為0到4]

            a[0] = b*c;
            a[1] = b+c;
            a[3] = a[1] * d;
            a[4] = a[3] * e;
            a[2] = d*e;

            在某些體系結(jié)構(gòu)中,剛剛計(jì)算完的a[1]可以放到寄存器中,以提高實(shí)際的計(jì)算性能。上述5個(gè)計(jì)算任務(wù)之間,只有1, 3, 4三個(gè)計(jì)算任務(wù)必須串行地執(zhí)行,因此,在新的處理器上,這樣做甚至能夠提高程序的并行度,從而使程序效率變得更高。

            5.3 全局寄存器優(yōu)化

            [待修訂內(nèi)容] 本章中,從這一節(jié)開始的所有優(yōu)化都是在微觀層面上的優(yōu)化了。換言之,這些優(yōu)化是不能使用高級語言中的對應(yīng)設(shè)施進(jìn)行解釋的。這一部分內(nèi)容將進(jìn)行較大規(guī)模的修訂。

            通常,此類優(yōu)化是由編譯器自動(dòng)完成的。我個(gè)人并不推薦真的由人來完成這些工作——這些工作多半是枯燥而重復(fù)性的,編譯器通常會比人做得更好(沒說的,肯定也更快)。但話說回來,使用匯編語言的程序設(shè)計(jì)人員有責(zé)任了解這些內(nèi)容,因?yàn)橹挥羞@樣才能更好地駕馭處理器。

            在前面的幾章中我已經(jīng)提到過,寄存器的速度要比內(nèi)存快。因此,在使用寄存器方面,編譯器一般會做一種稱為全局寄存器優(yōu)化的優(yōu)化。

            例如,在我們的程序中使用了4個(gè)變量:i, j, k, l。它們都作為循環(huán)變量使用:

            for(i=0; i<1000; i++){
            ? for(j=0; j<1000; j++){
            ??? for(k=0; k<1000; k++){
            ????? for(l=0; l<1000; l++)
            ??????? do_something(i, j, k, l);
            ??? }
            ? }
            }

            這段程序的優(yōu)化就不那么簡單了。顯然,按照通常的壓棧方法,i, j, k, l應(yīng)該按照某個(gè)順序被壓進(jìn)堆棧,然后調(diào)用do_something(),然后函數(shù)做了一些事情之后返回。問題在于,無論如何壓棧,這些東西大概都得進(jìn)內(nèi)存(不可否認(rèn)某些機(jī)器可以用CPU的Cache做這件事情,但Cache是寫通式的和回寫式的又會造成一些性能上的差異)。

            聰明的讀者馬上就會指出,我們不是可以在定義do_something()的時(shí)候加上inline修飾符,讓它在本地展開嗎?沒錯(cuò),本地展開以增加代碼量為代價(jià)換取性能,但這只是問題的一半。編譯器盡管完成了本地展開,但它仍然需要做許多額外的工作。因?yàn)榧拇嫫髦挥心敲从邢薜膸讉€(gè),而我們卻有這么多的循環(huán)變量。

            把四個(gè)變量按照它們在循環(huán)中使用的頻率排序,并決定在do_something()塊中的優(yōu)先順序(放入寄存器中的優(yōu)先順序)是一個(gè)解決方案。很明顯,我們可以按照l, k, j, i的順序(從高到低,因?yàn)閘將被進(jìn)行1000*1000*1000*1000次運(yùn)算!)來排列,但在實(shí)際的問題中,事情往往沒有這么簡單,因?yàn)槟悴恢纃o_something()中做的到底是什么。而且,憑什么就以for(l=0; l<1000; l++)作為優(yōu)化的分界點(diǎn)呢?如果do_something()中還有循環(huán)怎么辦?

            如此復(fù)雜的計(jì)算問題交給計(jì)算機(jī)來做通常會有比較滿意的結(jié)果。一般說來,編譯器能夠?qū)Τ绦蛑凶兞康氖褂眠M(jìn)行更全面地估計(jì),因此,它分配寄存器的結(jié)果有時(shí)雖然讓人費(fèi)解,但卻是最優(yōu)的(因?yàn)橛?jì)算機(jī)能夠進(jìn)行大量的重復(fù)計(jì)算,并找到最好的方法;而人做這件事相對來講比較困難)。

            編譯器在許多時(shí)候能夠作出相當(dāng)讓人滿意的結(jié)果。考慮以下的代碼:

            int a=0;

            for(int i=1; i<10; i++)
            ? for(int j=1; j<100; j++){
            ??? a += (i*j);
            ? }

            讓我們把它變?yōu)槟撤N形式的中間代碼:

            00: 0 -> a
            01: 1 -> i
            02: 1 -> j
            03: i*j -> t
            04: a+t -> a
            05: j+1 -> j
            06: evaluate j < 100
            07: TRUE? goto 03
            08: i+1 -> i
            09: evaluate i < 10
            10: TRUE? goto 02
            11: [繼續(xù)執(zhí)行程序的其余部分]

            程序中執(zhí)行強(qiáng)度最大的無疑是03到05這一段,涉及的需要寫入的變量包括a, j;需要讀出的變量是i。不過,最終的編譯結(jié)果大大出乎我們的意料。下面是某種優(yōu)化模式下Visual C++ 6.0編譯器生成的代碼(我做了一些修改):

            xor eax, eax?????????????? ; a=0(eax: a)
            mov edx, 1???????????????? ; i=1(edx: i)
            push esi?????????????????? ; 保存esi(最后要恢復(fù),esi作為代替j的那個(gè)循環(huán)變量)
            nexti:
            mov ecx, edx?????????????? ; [t=i]
            mov esi, 999?????????????? ; esi=999: 此處修改了原程序的語義,但仍為1000次循環(huán)。
            nextj:
            add eax, ecx?????????????? ; [a+=t]
            add ecx, edx?????????????? ; [t+=i]
            dec esi??????????????????? ; j--
            jne SHORT nextj??????????? ; jne 等價(jià)于 jnz. [如果還需要,則再次循環(huán)]
            inc edx??????????????????? ; i++
            cmp edx, 10???????????? ?? ; i與10比較
            jl SHORT nexti???????????? ; i < 10, 再次循環(huán)
            pop esi??????????????????? ; 恢復(fù)esi

            這段代碼可能有些令人費(fèi)解。主要是因?yàn)樗粌H使用了大量寄存器,而且還包括了5.2節(jié)中曾提到的子表達(dá)式提取技術(shù)。表面上看,多引入的那個(gè)變量(t)增加了計(jì)算時(shí)間,但要注意,這個(gè)t不僅不會降低程序的執(zhí)行效率,相反還會讓它變得更快!因?yàn)橥瑯拥玫搅擞?jì)算結(jié)果(本質(zhì)上,i*j即是第j次累加i的值),但這個(gè)結(jié)果不僅用到了上次運(yùn)算的結(jié)果,而且還省去了乘法(很顯然計(jì)算機(jī)計(jì)算加法要比計(jì)算乘法快)。

            這里可能會有人問,為什么要從999循環(huán)到0,而不是按照程序中寫的那樣從0循環(huán)到999呢?這個(gè)問題和匯編語言中的取址有關(guān)。在下兩節(jié)中我將提到這方面的內(nèi)容。

            5.4 x86體系結(jié)構(gòu)上的并行最大化和指令封包

            考慮這樣的問題,我和兩個(gè)同伴現(xiàn)在在山里,遠(yuǎn)處有一口井,我們帶著一口鍋,身邊是樹林;身上的飲用水已經(jīng)喝光了,此處允許砍柴和使用明火(當(dāng)然我們不想引起火災(zāi):),需要燒一鍋水,應(yīng)該怎么樣呢?

            一種方案是,三個(gè)人一起搭灶,一起砍柴,一起打水,一起把水燒開。

            另一種方案是,一個(gè)人搭灶,此時(shí)另一個(gè)人去砍柴,第三個(gè)人打水,然后把水燒開。

            這兩種方案畫出圖來是這樣:

            o_7_1.gif

            僅僅這樣很難說明兩個(gè)方案孰優(yōu)孰劣,因?yàn)槲覀儾⒉幻鞔_三個(gè)人一起打水、一起砍柴、一起搭灶的效率更高,還是分別作效率更高(通常的想法,一起做也許效率會更高)。但假如說,三個(gè)人一個(gè)只會搭灶,一個(gè)只會砍柴,一個(gè)只會打水(當(dāng)然是說這三件事情),那么,方案2的效率就會搞一些了。

            在現(xiàn)實(shí)生活中,某個(gè)人擁有專長是比較普遍的情況;在設(shè)計(jì)計(jì)算機(jī)硬件的時(shí)候則更是如此。你不可能指望加法器不做任何改動(dòng)就能去做移位甚至整數(shù)乘法,然而我們注意到,串行執(zhí)行的程序不可能在同一時(shí)刻同時(shí)用到處理器的所有功能,因此,我們(很自然地)會希望有一些指令并行地執(zhí)行,以充分利用CPU的計(jì)算資源。

            CPU執(zhí)行一條指令的過程基本上可以分為下面幾個(gè)階段:取指令、取數(shù)據(jù)、計(jì)算、保存數(shù)據(jù)。假設(shè)這4個(gè)階段各需要1個(gè)時(shí)鐘周期,那么,只要資源夠用,并且4條指令之間不存在串行關(guān)系(換言之這些指令的執(zhí)行先后次序不影響最終結(jié)果,或者,更嚴(yán)格地說,沒有任何一條指令依賴其他指令的運(yùn)算結(jié)果)指令也可以像下面這樣執(zhí)行:

            指令1取指令取數(shù)據(jù)計(jì) 算存數(shù)據(jù)   
            指令2 取指令取數(shù)據(jù)計(jì) 算存數(shù)據(jù)  
            指令3  取指令取數(shù)據(jù)計(jì) 算存數(shù)據(jù) 
            指令4   取指令取數(shù)據(jù)計(jì) 算存數(shù)據(jù)

            這樣,原本需要16個(gè)時(shí)鐘周期才能夠完成的任務(wù)就可以在7個(gè)時(shí)鐘周期內(nèi)完成,時(shí)間縮短了一半還多。如果考慮灰色的那些方格(這些方格可以被4條指令以外的其他指令使用,只要沒有串行關(guān)系或沖突),那么,如此執(zhí)行對于性能的提升將是相當(dāng)可觀的(此時(shí),CPU的所有部件都得到了充分利用)。

            當(dāng)然,作為程序來說,真正做到這樣是相當(dāng)理想化的情況。實(shí)際的程序中很難做到徹底的并行化。假設(shè)CPU能夠支持4條指令同時(shí)執(zhí)行,并且,每條指令都是等周期長度的4周期指令,那么,程序需要保證同一時(shí)刻先后發(fā)射的4條指令都能夠并行執(zhí)行,相互之間沒有關(guān)聯(lián),這通常是不太可能的。

            最新的Intel Pentium 4-XEON處理器,以及Intel Northwood Pentium 4都提供了一種被稱為超線程(Hyper-Threading TM)的技術(shù)。該技術(shù)通過在一個(gè)處理器中封裝兩組執(zhí)行機(jī)構(gòu)來提高指令并行度,并依靠操作系統(tǒng)的調(diào)度來進(jìn)一步提升系統(tǒng)的整體效率。

            由于線程機(jī)制是與操作系統(tǒng)密切相關(guān)的,因此,在本文的這一部分中不可能做更為深入地探討。在后續(xù)的章節(jié)中,我將介紹Win32、FreeBSD 5.x以及Linux中提供的內(nèi)核級線程機(jī)制(這三種操作系統(tǒng)都支持SMP及超線程技術(shù),并且以線程作為調(diào)度單位)在匯編語言中的使用方法。

            關(guān)于線程的討論就此打住,因?yàn)樗嗟匾蕾囉诓僮飨到y(tǒng),并且,無論如何,操作系統(tǒng)的線程調(diào)度需要更大的開銷并且,到目前為止,真正使用支持超線程的CPU,并且使用相應(yīng)操作系統(tǒng)的人是非常少的。因此,我們需要關(guān)心的實(shí)際上還是同一執(zhí)行序列中的并發(fā)執(zhí)行和指令封包。不過,令人遺憾的是,實(shí)際上在這方面編譯器做的幾乎是肯定要比人好,因此,你需要做的只是開啟相應(yīng)的優(yōu)化;如果你的編譯器不支持這樣的特性,那么就把它扔掉……據(jù)我所知,目前在Intel平臺上指令封包方面做的最好的是Intel的C++編譯器,經(jīng)過Intel編譯器編譯的代碼的性能令人驚異地高,甚至在AMD公司推出的兼容處理器上也是如此。

            5.5 存儲優(yōu)化

            從前一節(jié)的圖中我們不難看出,方案2中,如果誰的動(dòng)作慢,那么他就會成為性能的瓶頸。實(shí)際上,CPU也不會像我描述的那樣四平八穩(wěn)地運(yùn)行,指令執(zhí)行的不同階段需要的時(shí)間(時(shí)鐘周期數(shù))是不同的,因此,縮短關(guān)鍵步驟(即,造成瓶頸的那個(gè)步驟)是縮短執(zhí)行時(shí)間的關(guān)鍵。

            至少對于使用Intel系列的CPU來說,取數(shù)據(jù)這個(gè)步驟需要消耗比較多的時(shí)間。此外,假如數(shù)據(jù)跨越了某種邊界(如4或8字節(jié),與CPU的字長有關(guān)),則CPU需要啟動(dòng)兩次甚至更多次數(shù)的讀內(nèi)存操作,這無疑對性能構(gòu)成不利影響。

            基于這樣的原因,我們可以得到下面的設(shè)計(jì)策略:


            程序設(shè)計(jì)中的內(nèi)存數(shù)據(jù)訪問策略

            • 盡可能減少對于內(nèi)存的訪問。在不違背這一原則的前提下,如果可能,將數(shù)據(jù)一次處理完。
            • 盡可能將數(shù)據(jù)按4或8字節(jié)對齊,以利于CPU存取
            • 盡可能一段時(shí)間內(nèi)訪問范圍不大的一段內(nèi)存,而不同時(shí)訪問大量遠(yuǎn)距離的分散數(shù)據(jù),以利于Cache緩存*

            第一條規(guī)則比較簡單。例如,需要求一組數(shù)據(jù)中的最大值、最小值、平均數(shù),那么,最好是在一次循環(huán)中做完。

            “于是,這家伙又?jǐn)€了一段代碼”……

            int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};
            int i;
            int avg, max, min;

            avg=max=min=a[0];

            for(i=1; i<(sizeof(a)/sizeof(int)); i++){
            ? avg+=a[i];
            ? if(max < a[i])
            ??? max = a[i];
            ? elseif(min > a[i])
            ??? min = a[i];
            }

            avg /= i;

            Visual C++編譯器把最開始一段賦值語句翻譯成了一段簡直可以說是匪夷所思的代碼:

            ; int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};

            mov edi, 2???????????????????????? ; 此時(shí)edi沒有意義
            mov esi, 3???????????????????????? ; esi也是!臨時(shí)變量而已。
            mov DWORD PTR _a$[esp+92], edi
            mov edx, 5???????????????????????? ; 黑名單加上edx
            mov eax, 7???????????????????????? ; eax也別跑:)
            mov DWORD PTR _a$[esp+132], edi
            mov ecx, 9???????????????????????? ; 就差你了,ecx

            ; int i;
            ; int avg, max, min;
            ; avg=max=min=a[0];


            mov edi, 1???????????????????????? ; edi搖身一變,現(xiàn)在它是min了。
            mov DWORD PTR _a$[esp+96], esi
            mov DWORD PTR _a$[esp+104], edx
            mov DWORD PTR _a$[esp+112], eax
            mov DWORD PTR _a$[esp+136], esi
            mov DWORD PTR _a$[esp+144], edx
            mov DWORD PTR _a$[esp+152], eax
            mov DWORD PTR _a$[esp+88], 1?????? ; 編譯器失誤? 此處edi應(yīng)更好
            mov DWORD PTR _a$[esp+100], 4
            mov DWORD PTR _a$[esp+108], 6
            mov DWORD PTR _a$[esp+116], 8
            mov DWORD PTR _a$[esp+120], ecx
            mov DWORD PTR _a$[esp+124], 0
            mov DWORD PTR _a$[esp+128], 1
            mov DWORD PTR _a$[esp+140], 4
            mov DWORD PTR _a$[esp+148], 6
            mov DWORD PTR _a$[esp+156], 8
            mov DWORD PTR _a$[esp+160], ecx
            mov DWORD PTR _a$[esp+164], 0
            mov edx, edi????????????????????? ?; edx是max。
            mov eax, edi????????????????????? ?; 期待已久的avg, 它被指定為eax

            這段代碼是最優(yōu)的嗎?我個(gè)人認(rèn)為不是。因?yàn)榫幾g器完全可以在編譯過程中直接把它們作為常量數(shù)據(jù)放入內(nèi)存。此外,如果預(yù)先對a[0..9]10個(gè)元素賦值,并利用串操作指令(rep movsdw),速度會更快一些。

            當(dāng)然,犯不上因?yàn)檫@些問題責(zé)怪編譯器。要求編譯器知道a[0..9]和[10..19]的內(nèi)容一樣未免過于苛刻。我們看看下面的指令段:

            ; for(i=1; ...

            mov esi, edi
            for_loop:

            ; avg+=a[i];

            mov ecx, DWORD PTR _a$[esp+esi*4+88]
            add eax, ecx

            ; if(max < a[i])

            cmp edx, ecx
            jge SHORT elseif_min

            ; max = a[i];

            mov edx, ecx

            ; else if(min > a[i])

            jmp SHORT elseif_min
            elseif_min:
            cmp edi, ecx
            jle SHORT elseif_end

            ; min = a[i];
            mov edi, ecx

            elseif_end:

            ; [for i=1]; i<20; i++){

            inc esi
            cmp esi, 20
            jl SHORT for_loop

            ; }
            ; avg /= i;


            cdq
            idiv esi


            ; esi: i




            ; ecx: 暫存變量, =a[i]
            ; eax: avg



            ; edx: max








            ; 有趣的代碼...并不是所有的時(shí)候都有用
            ; 但是也別隨便刪除
            ; edi: min









            ; i++
            ; i與20比較






            ; avg /= i

            上面的程序倒是沒有什么驚人之處。唯一一個(gè)比較嚇人的東西是那個(gè)jmp SHORT指令,它是否有用取決于具體的問題。C/C++編譯器有時(shí)會產(chǎn)生這樣的代碼,我過去曾經(jīng)錯(cuò)誤地把所有的此類指令當(dāng)作沒用的代碼而刪掉,后來發(fā)現(xiàn)程序執(zhí)行時(shí)間沒有明顯的變化。通過查閱文檔才知道,這類指令實(shí)際上是“占位指令”,他們存在的意義在于占據(jù)那個(gè)地方,一來使其他語句能夠正確地按CPU覺得舒服的方式對齊,二來它可以占據(jù)CPU的某些周期,使得后續(xù)的指令能夠更好地并發(fā)執(zhí)行,避免沖突。另一個(gè)比較常見的、實(shí)現(xiàn)類似功能的指令是NOP。

            占位指令的去留主要是靠計(jì)時(shí)執(zhí)行來判斷。由于目前流行的操作系統(tǒng)基本上都是多任務(wù)的,因此會對計(jì)時(shí)的精確性有一定影響。如果需要進(jìn)行測試的話,需要保證以下幾點(diǎn):


            計(jì)時(shí)測試需要注意的問題

            • 測試必須在沒有額外負(fù)荷的機(jī)器上完成。例如,專門用于編寫和調(diào)試程序的計(jì)算機(jī)
            • 盡量終止計(jì)算機(jī)上運(yùn)行的所有服務(wù),特別是殺毒程序
            • 切斷計(jì)算機(jī)的網(wǎng)絡(luò),這樣網(wǎng)絡(luò)的影響會消失
            • 將進(jìn)程優(yōu)先級調(diào)高。對于Windows系統(tǒng)來說,把進(jìn)程(線程)設(shè)置為Time-Critical; 對于*nix系統(tǒng)來說,把進(jìn)程設(shè)置為實(shí)時(shí)進(jìn)程
            • 將測試函數(shù)運(yùn)行盡可能多次運(yùn)行,如10000000次,這樣能夠減少由于進(jìn)城切換而造成的偶然誤差
            • 最后,如果可能的話,把函數(shù)放到單進(jìn)程的系統(tǒng)(例如FreeDOS)中運(yùn)行。

            對于絕大多數(shù)程序來說,計(jì)時(shí)測試是一個(gè)非常重要的東西。我個(gè)人傾向于在進(jìn)行優(yōu)化后進(jìn)行計(jì)時(shí)測試并比較結(jié)果。目前,我基于經(jīng)驗(yàn)進(jìn)行的優(yōu)化基本上都能夠提高程序的執(zhí)行性能,但我還是不敢過于自信。優(yōu)化確實(shí)會提高性能,但人做的和編譯器做的思路不同,有時(shí),我們的確會做一些費(fèi)力不討好的事情。

            posted on 2006-11-06 10:39 編程之道 閱讀(364) 評論(0)  編輯 收藏 引用 所屬分類: 開發(fā)相關(guān)ASM

            2021久久精品免费观看| 国产无套内射久久久国产| 狠狠人妻久久久久久综合蜜桃 | 久久久久亚洲精品日久生情| 亚洲天堂久久精品| 国产精品视频久久久| 久久精品无码专区免费东京热| 久久婷婷五月综合成人D啪 | 精品久久久久久中文字幕人妻最新| 久久只有这里有精品4| 2021国产精品午夜久久| 久久人妻无码中文字幕| 2021国内久久精品| 色诱久久久久综合网ywww| 久久精品国产亚洲av麻豆小说 | 97久久国产露脸精品国产| 久久人人爽人人人人爽AV| 伊人久久大香线蕉综合影院首页| 少妇人妻88久久中文字幕| 俺来也俺去啦久久综合网| 品成人欧美大片久久国产欧美...| 品成人欧美大片久久国产欧美| 久久精品综合一区二区三区| 思思久久好好热精品国产| 国内精品综合久久久40p| 国产精品久久久久久久| 国内精品久久久久久久coent| 亚洲欧洲精品成人久久奇米网| 欧美黑人又粗又大久久久| 国产欧美久久久精品| 日韩AV毛片精品久久久| 婷婷久久久亚洲欧洲日产国码AV| 26uuu久久五月天| 狠狠综合久久AV一区二区三区| 97久久精品午夜一区二区| 热久久视久久精品18| 色综合久久精品中文字幕首页 | 99久久综合国产精品免费| 精品一区二区久久| 久久久久久久综合狠狠综合| 日韩精品国产自在久久现线拍|