交換兩個變數效率問題

2021-05-22 08:09:11 字數 4457 閱讀 8201

首先宣告,在物件導向盛行的時代裡,我改用物件這兩個詞來指代最廣泛的變數。 現在的變數就不一定只是乙個整型或浮點型,甚至不是乙個基本資料型別。我們 將在更廣泛的意義上討論物件交換的問題。

在前一篇文章 「關於兩個物件交換的問題」(注意,名稱已改)中,我們討論了交換兩個變數 的幾種方法,並給出了形式化的公式。而在這一篇文章中,我們將討論的是效率 與可行性的問題。(注:這個主題的想法,主要是受farproc

首先,我們來看採用最簡單直接的交換方式的**:

按語言本身的特性來想,這些**做以下這些工作:

在棧上分配為整型變數tmp分配空間;

將a的值放入tmp中;

將b的值放入a中;

將tmp的值放入b中;

釋放為tmp分配的棧空間。

而實際上呢?我們來看看生成的彙編**:

movl        b, %eax    ;將b從記憶體載入到暫存器eax

movl a, %edx ;將a從記憶體載入到暫存器edx

movl %eax, a ;將eax的內容存入到記憶體a中

xorl %eax, %eax ;將eax清零

movl %edx, b ;將edx的內容存入到記憶體b中

看起來,彙編指令並不象我們想象的那樣複雜。因為變數要參與運算首先要從內 存載入到暫存器中,所以要將兩個變數交換只需按相反的順序再存入到記憶體中就 可以了。只是四個記憶體與暫存器之間交換資料的指令,看起來好像沒有交換操作 似的。而此處為什麼要將eax清零呢?因為eax暫存器是專門用來放函式返回值 的,而我們的測試函式很簡單,除了執行上面的操作外,剩下的就是return 0;了,因此它與變數交換根本沒有關係。從上面可以看到,編譯器為我們做的工 作遠比我們想像的要多。

接下來,我們來看基於異或方式交換的**:

這一**看起來很純粹,沒有一句是浪費的(是指全部操作都與交換有關,沒有 像上例中的分配臨時變數空間的操作),而且**直接對應操作:三次異或。憑 著直覺,我們覺得它應該是效率最高的。但是它帶來的***是**的可讀性大 大降低(注意,可讀性很重要),而一些人認為這是值得的,因為它帶來的效率。 我們接下來看看究竟是不是值得的。

下面是上面**對應的彙編**:

movl        b, %eax       ;將b從記憶體載入暫存器eax

movl a, %ecx ;將a從記憶體載入暫存器ecx

movl %eax, %edx ;將eax的值儲存到edx中

xorl %ecx, %edx ;ecx與edx異或

xorl %edx, %eax ;edx與eax異或

xorl %eax, %edx ;eax與edx異或

movl %eax, b ;將eax的值存入到記憶體b中

xorl %eax, %eax ;將eax置0:設定返回值,與上例中一樣

movl %edx, a ;將edx的值存入到記憶體a中

哦,好像有點暈了。

它總共用了四次記憶體與暫存器之間的資料移動操作,一次暫存器之間的賦值,以 及三次異或運算。

我很詫異編譯器會產生這樣的彙編**,我懷疑是編譯選項出了問題(這是在-o2下 的結果),於是試了-o3的結果,居然也是完全一樣,更令人意想不到的 是,在-o1下產生的結果居然是最簡潔的。不過我們先來看上面這些**都做了些 什麼操作,是否都是必要的操作。

首先我們將上面的c**改寫一下(現在想來才覺得c**其實也是一樣的迷惑 人,我並不清楚它到底經過了哪些步驟,而只知道它能交換兩個整型變數的值而 已):

現在,我們來將彙編**逐行翻譯為c**來看看(忽略記憶體與暫存器之間的資料 交換):

int tmp;        //暫存器edx對應變數tmp

tmp = b;

tmp = a ^ tmp; //對應於tmp = a ^ b;

b = tmp ^ b;

tmp = b ^ tmp;

a = tmp; //對應於a = tmp ^ b;

與我們轉換後的**相比,對這段**編譯器好像有點犯迷糊了。我們明明沒有 用中間變數的**,它居然不僅用了中間變數,而且還多用了兩個賦值操作。

接下來我們再看在-o1下產生的結果:

movl        b, %eax       ;將b載入到暫存器eax

movl %eax, %edx ;將eax的值儲存到edx

xorl a, %edx ;記憶體a與edx異或,結果儲存到edx,得到中間結果

xorl %edx, %eax ;edx與eax異或,結果到eax,得到b的最終值,即a

movl %eax, b ;儲存到記憶體b

xorl %eax, %edx ;edx與eax異或,結果到edx,得到a的最終值,即b

movl %edx, a ;儲存到記憶體a

movl $0, %eax ;設定返回值

這一結果與我們手工轉換的**是類似的。但它不僅進行了四次記憶體與暫存器之 間的資料移動操作(對應於中間變數交換的情況),而且還進行了一次暫存器之 間的賦值,兩次暫存器之間的異或運算,以及一次暫存器與記憶體之間的異或運算 (應該包含一次記憶體與隱含暫存器之間的資料移動,以及一次異或運算)。由此 看來,-o1產生的**確實不如-o2產生的**效率高,編譯器並沒有犯迷糊。

很明顯可以看出,異或方式的效率比預期的要壞得多,而且要比採用中間變數的 方式更壞。現在看來,如果我們一開始就從彙編及cpu的執行流程上來考慮的話, 就可以很容易的得出這一結論。在機器的角度來考慮交換兩個整型變數(即相對 應的記憶體)的值,只需要將兩個變數的值載入到暫存器中,然後按相反的對應關 系使用,或是按相反的對應關係儲存到記憶體中即可,完全不需要經過中間計算。 而用異或方式,除了上述記憶體與暫存器之間的資料移動操作外,還需要進行三次 的異或操作(以及可能由此帶來的移動操作)。這個結論是顯而易見的。

採用異或的方式,我們不僅犧牲了可讀性,而且還犧牲了效率,所以並不可取。

其它的方式,如加、乘等,用腳趾頭想想也知道結果了,所以就不再討論了。

以上的結果,只是根據由c**生成的彙編**的行數,及其記憶體與暫存器之間數 據移動的次數等方面比較它們的效率;c**也是很簡單而純粹的整型變數交換, 與實際情況差別較大;而且最重要的是沒有來實際測量它們的執行時間,因此得出 的結論並不一定正確。

本次只討論的是對整型變數交換的情況,而實際中要交換的物件是多種多樣的。 比如在c++中,最常見的應該就是類物件的交換,甚至是兩個不知道何種型別的對 象的交換(考慮模板類的情形)。

並不是所有物件都支援異或、加、乘的運算,所以這些方法就基本捨棄了,但仍要 重視它們所帶來的思想上的東西(這種情況下仍然有可以用它們,但是很危險, 參見注1)。而基於中間變數的方式也要加以小心,一些對 象必須提供合適的拷貝建構函式和賦值運算子函式,才能保證交換操作在語義上 也是正確的,比如那些內部含有指標成員的類物件。

總的來說,採用中間變數方式交換兩個物件的值,是最通用、可讀性最高、效 率比較高的一種方式。在此我建議大家在一般情況下,都採用這種方式。 (注2)

[1]我們可以將物件看成若干個字元型別變數的陣列,從而可以使用異或等方式。 但是,這並不能保證它的語義是正確的,尤其是在c++中。可以這樣說,在實際情 況中,這樣的操作幾乎總是會帶來錯誤。

[2]說到最後,還不如原來就不要知道這種方法呢:)

[n]我的系統平台是debian 4.1.1、gcc 4.1.2,所有編譯選項預設均為-o2,編譯為 彙編**的選項為-s。

經過我的測試(vc2005 release),使用乙個臨時變數的交換方式還是效率最高的。位異或的次之,相加或相乘的最慢。

其實看一下生成的彙編碼就很清楚了。

使用臨時變數版本:

mov eax,edi

mov edi,esi

mov esi,eax

位異或版本:

xor edi,esi

xor esi,edi

xor edi,esi

加減版本:

add edi,esi

mov ecx,edi

sub ecx,esi

mov esi,ecx

sub edi,esi

[n+2]思想在交流中迸發:kebing.zhgmailcom

交換兩個變數

目的 實現兩個變數值的交換 以int 型別為例 方法一 使用臨時變數。優點 安全,適合任何型別資料交換,無溢位風險。缺點 保守 效率不高,需要使用第三方臨時變數 棧空間 1 void swap value int var1,int var2 2 方法二 利用 算術運算實現。優點 不需要額外的臨時變數...

swap交換兩個變數

最簡單的交換兩個變數值是用指標。void swap int a,int b work 這裡有個問題,一定要想清楚。為什麼在函式體內不能交換指標了?變成這樣 void swap int a,int b will not work在swap函式裡,a and b 都會產生乙個copy來,那你tmp a ...

交換兩個變數方法

將兩個變數的值互換,相當簡單的問題。假設有變數a b int a int b 方法1 將a b的值互換,為 int tmp a a b b tmp 然而,如果要求不用中間變數,就交換變數的值,該怎麼做呢?乙個比較有效率且眾所周知的方法就是 方法2 三次異或操作 int a 10,b 12 a 101...