【轉】Duff's Device

在看strcpy、memcpy等的實現發現用了內存對齊,每個word拷貝一次的辦法大大提升了實現效率,參加該blog(http://totoxian.iteye.com/blog/1220273)。

duff's device也是利用了相似的原理減小比較的次數來提升了效率。ios

 

前幾天在網上看見了一段代碼,叫作「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 ( -- n  > 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>中實現可變長度的數組的思想那樣,節省了大量的機器資源,也大大提升了程序的效率。這是值得咱們學習的。
相關文章
相關標籤/搜索