達夫設備的詳細說明

轉自:http://blog.csdn.net/nicky_zs/archive/2008/03/19/2196803.aspx

前幾天在網上看見了一段代碼,叫做“Duff's Device”,後經驗證它曾出現在Bjarne的TC++PL裏面:

void send( int * to, int * from, int count)
        
//    Duff設施,有幫助的註釋被有意刪去了 
{
        
int n = (count + 7 ) / 8 ;
        
switch (count % 8 ) {
        
case 0 :    do { * to ++ = * from ++ ;
        
case 7 :          * to ++ = * from ++ ;
        
case 6 :          * to ++ = * from ++ ;
        
case 5 :          * to ++ = * from ++ ;
        
case 4 :          * to ++ = * from ++ ;
        
case 3 :          * to ++ = * from ++ ;
        
case 2 :          * to ++ = * from ++ ;
        
case 1 :          * to ++ = * from ++ ;
                }
while ( -- n >    0 );
        }
 
}
  


代碼的結構顯得非常巧妙,把一個switch語句和一個do-while語句糅合在了一起。而在我看過的所有關於C和C++的書中,這樣的代碼都 是毫無道理的。然而,無論是在VS2005還是在GCC4.1.2下,這段代碼都能正確地通過編譯。加上適當的main函數,它都可以正常運行。我百思不 得其解。上網去查,也沒查到好答案。

怎麼辦?先看看它的彙編代碼吧,也許可以通過它的彙編代碼看出它的意思。

gcc -S send.cpp

粗略地一看,彙編代碼都已經上百行了,而且裏面還有一個跳轉表,十幾個標號。一般情況下,幾十行的彙編代碼都已經不太好看懂了,要把這幾百行彙編完全看懂,估計需要花很多時間。

既然直接來太麻煩,那就用簡便一點的方法吧:

#include  < iostream >
using   namespace  std;

int  main()
{
    
int  n  = 0 ;
    
switch  (n) 
    
case   0 do   {cout  <<   " 0 "   <<  endl;
    
case 1 :         cout  <<   " 1 "   <<  endl;
    
case 2 :         cout  <<   " 2 "   <<  endl;
    
case   3 :         cout  <<   " 3 "   <<  endl;
            }
  while ( -- > 0 ); 
    }

}

 

實驗結果
n的值 程序輸出
0 0
1
2
3
1 1
2
3
2 2
3
0
1
2
3
3 3
0
1
2
3
0
1
2
3
其他 (無輸出)



































這 下終於弄清楚了。原來,那段代碼的主體還是do-while循環,但這個循環的入口點並不一定是在do那裏,而是由這個switch語句根據n,把循環的 入口定在了幾個case標號那裏。也就是說,程序的執行流程是:程序一開始順序執行,當它執行到了switch的時候,就會根據n的值,直接跳轉到 case n那裏(從此,這個swicth語句就再也沒有用了)。程序繼續順序執行,再當它執行到while那裏時,就會判斷循環條件。若爲真,則while循環開 始,程序跳轉到do那裏開始執行循環(這時候由於已經沒有了switch,所以後面的標號就變成普通標號了,即在沒有goto語句的情況下就可以忽略掉這 些標號了);爲假,則退出循環,即程序中止。

忙活了幾個小時,終於明白這段代碼是怎麼回事了。回想一下,自己以前也曾寫過類似C的語法但比C語法簡單很多的解釋器,用的是遞歸子程序法。而如果用遞歸下降法來分析這段代碼,是肯定會有問題的。

至於它是怎麼正確編譯並運行的,這需要去研究一下C編譯器,這個以後再說。現在,還是再來看看達夫設備吧。其實,這個send函數的簽名就已經很具有提示性了:把from數組中的元素拷貝count個到to裏面去。於是有人會說,這個工作簡單,不就這樣嗎:

void my_send( int   * to,  int   * from,  int  count)
{
    
for  ( int  i  =   0 ; i  !=  count;  ++ i)  {
        
* to ++   =   * from ++ ;
    }

}


這段代碼的確很簡潔,也是正確的,而且生成的機器碼也比send函數短很多。但是卻忽略了一個因素:執行效率。計算一下就可以知 道,my_send函數裏面的循環條件,即i和count的比較運算的次數,是達夫設備的8倍!在做整數賦值這種耗時很少的工作時,這種耗時相對較高的比 較工作是會大大地影響函數整體的效率的。達夫設備則是一種非常巧妙的解決辦法(當然,它利用到了編譯器的一些實現上的工作),而且如果把8換成更大的數的 話,效率就還可以提高!

它的思路是這樣的:把原數組以8個int爲單位分成若干個小組,複製的時候以小組爲單位複製,即一次複製8個 int。也就是說,在my_send函數中以一次比較運算的代價換來1個int的複製,而在達夫設備中,卻能以一次比較運算的代價換來8個int的複製。 而switch語句則是用來處理分組時剩下的不到8個的int(這些剩餘的不是數組最後的,而是數組最開始的),很巧妙。

總結:像達夫設 備這樣的代碼,從語言的角度來看,我個人覺得不值得我們借鑑。因爲這畢竟不是“正常”的代碼,至少C/C++標準不會保證這樣的代碼一定不會出錯。另外, 這種代碼估計有很多人根本都沒見過,如果自己寫的代碼別人看不懂,這也會是一件很讓人頭疼的事。然而,從算法的角度來看,我覺得達夫設備是個很高效、很值 得我們去學習的東西。把一次消耗相對比較高的操作“分攤“到了多次消耗相對比較低的操作上面,就像vector<T>中實現可變長度的數組的 思想那樣,節省了大量的機器資源,也大大提高了程序的效率。這是值得我們學習的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章