這個練習是一個腦筋急轉彎,我會向你介紹最著名的C語言黑魔法之一,叫做“達夫設備”,以“發(fā)明者”湯姆·達夫的名字命名。這一強大(或邪惡?)的代碼中,幾乎你學過的任何東西都被包裝在一個小的結構中。弄清它的工作機制也是一個好玩的謎題。
注
C的一部分樂趣來源于這種神奇的黑魔法,但這也是使C難以使用的地方。你最好能夠了解這些技巧,因為他會帶給你關于C語言和你計算機的深入理解。但是,你應該永遠都不要使用它們,并總是追求簡單易讀的代碼。
達夫設備由湯姆·達夫“發(fā)現”(或創(chuàng)造),它是一個C編譯器的小技巧,本來不應該能夠正常工作。我并不想告訴你做了什么,因為這是一個謎題,等著你來思考并嘗試解決。你需要運行這段代碼,之后嘗試弄清它做了什么,以及為什么可以這樣做。
#include <stdio.h>
#include <string.h>
#include "dbg.h"
int normal_copy(char *from, char *to, int count)
{
int i = 0;
for(i = 0; i < count; i++) {
to[i] = from[i];
}
return i;
}
int duffs_device(char *from, char *to, int count)
{
{
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);
}
}
return count;
}
int zeds_device(char *from, char *to, int count)
{
{
int n = (count + 7) / 8;
switch(count % 8) {
case 0:
again: *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++;
if(--n > 0) goto again;
}
}
return count;
}
int valid_copy(char *data, int count, char expects)
{
int i = 0;
for(i = 0; i < count; i++) {
if(data[i] != expects) {
log_err("[%d] %c != %c", i, data[i], expects);
return 0;
}
}
return 1;
}
int main(int argc, char *argv[])
{
char from[1000] = {'a'};
char to[1000] = {'c'};
int rc = 0;
// setup the from to have some stuff
memset(from, 'x', 1000);
// set it to a failure mode
memset(to, 'y', 1000);
check(valid_copy(to, 1000, 'y'), "Not initialized right.");
// use normal copy to
rc = normal_copy(from, to, 1000);
check(rc == 1000, "Normal copy failed: %d", rc);
check(valid_copy(to, 1000, 'x'), "Normal copy failed.");
// reset
memset(to, 'y', 1000);
// duffs version
rc = duffs_device(from, to, 1000);
check(rc == 1000, "Duff's device failed: %d", rc);
check(valid_copy(to, 1000, 'x'), "Duff's device failed copy.");
// reset
memset(to, 'y', 1000);
// my version
rc = zeds_device(from, to, 1000);
check(rc == 1000, "Zed's device failed: %d", rc);
check(valid_copy(to, 1000, 'x'), "Zed's device failed copy.");
return 0;
error:
return 1;
}
這段代碼中我編寫了三個版本的復制函數:
normal_copy
使用普通的for
循環(huán)來將字符從一個數組復制到另一個。
duffs_device
這個就是稱為“達夫設備”的腦筋急轉彎,以湯姆·達夫的名字命名。這段有趣的邪惡代碼應歸咎于他。
zeds_device
“達夫設備”的另一個版本,其中使用了goto
來讓你發(fā)現一些線索,關于duffs_device
中奇怪的do-while
做了什么。
在往下學習之前仔細了解這三個函數,并試著自己解釋代碼都做了什么。
這個程序沒有任何輸出,它只會執(zhí)行并退出。你應當在Valgrind
下運行它并確保沒有任何錯誤。
首先需要了解的一件事,就是C對于它的一些語法是弱檢查的。這就是你可以將do-while
的一部分放入switch
語句的一部分的原因,并且在其它地方的另一部分還可以正常工作。如果你觀察帶有goto again
的我的版本,它實際上更清晰地解釋了工作原理,但要確保你理解了這一部分是如何工作的。
第二件事是switch
語句的默認貫穿機制可以讓你跳到指定的case
,并且繼續(xù)運行知道switch
結束。
最后的線索是count % 8
以及頂端對n
的計算。
現在,要理解這些函數的工作原理,需要完成下列事情:
switch
之前初始化時,在紙的空白區(qū)域,把每個變量列在表中。switch
的邏輯模擬執(zhí)行代碼,之后再正確的case
處跳出。to
、from
和它們所指向的數組。while
或者我的goto
時,檢查你的變量,之后按照邏輯返回do-while
頂端,或者again
標簽所在的地方。當你弄明白它的實際工作原理時,最終的問題是:問什么要把代碼寫成這樣?這個小技巧的目的是手動編寫“循環(huán)展開”。大而長的循環(huán)會非常慢,所以提升速度的一個方法就是找到循環(huán)中某個固定的部分,之后在循環(huán)中復制代碼,序列化地展開。例如,如果你知道一個循環(huán)會執(zhí)行至少20次,你就可以將這20次的內容直接寫在源代碼中。
達夫設備通過將循環(huán)展開為8個迭代塊,來完成這件事情。這是個聰明的辦法,并且可以正常工作。但是目前一個好的編譯器也會為你完成這些。你不應該這樣做,除非少數情況下你證明了它的確可以提升速度。
case
語句,并且不想手動把它們都寫出來時,你會怎么辦?你可以編寫一次展開8個的宏嗎?main
函數,執(zhí)行一些速度檢測,來看看哪個實際上更快。memcpy
、memmove
和memset
,并且也比較一下它們的速度。