三、現有的邏輯編程譯者
我們將在本節詳細介紹Janus,KL1,Erlang和wamcc如何處理控制流程。此簡報的靈感來自[5],它採用了堆疊模型的目標。但是,我們不遵循類似於實際執行的抽象。這種選擇的後果,明確描述了C代碼與WAM指令的相關性。由於篇幅所限,我們只在這裡討論控制問題。首先是出於這樣的事實,wamcc使用的WAM是傳統而沒有最佳化的。從而現在為其他指令寫的代碼變得眾所周知了[1]。第二,有效控制的關鍵在於翻譯成C,因為WAM代碼是平的並且通過分支來執行轉換。這是更適合高層次的控制結構,例如功能,並且對於低層級控制不提供更多。因此,主要問題將為WAM分支的翻譯找到一個合適的解決方案。我們的示範將基於下面的例子中,只有一個子句和一個事實:
p: allocate /* p:- q, r. */ call(q) deallocate execute(r)q: proceed /* q. */
然而,這個簡單的例子顯示了Prolog的控制在確定性情況下使用的指令。翻譯的方式調用和執行將尤其突出,如何管理直接分枝(即當目標地址是一個已知的標籤),而進行指令翻譯解決問題間接分枝(即當目標地址是某些變數的內容,在這種情況下註冊CP)。
但是出現了問題,因為間接分支是不具備的標準(ANSI)C(因此必須類比),也因為GOTO指令只能處理在同一功能的代碼。該解決方案,因此導致一個C程式組成的一個獨特的功能一個開關指令來類比間接GOTO語句。這種方法之後,我們前面的例子將被轉換為:
fct_switch(){ label_switch: switch(PC){ case p: /* p:- q,r . */ label_p: push(CP); /* allocate */ CP=p1; /* call(q) */ goto label_q; /* " */ case p1: pop(CP); /* deallocate */ goto label_r; /* execute(r) */ case q: /* q. */ label_q: PC=CP; /* proceed */ goto label_switch; /* : */ . . . }}
這種方法在RISC機器花費大,因為switch語句的成本約為10個機器指令(包括邊界檢查)。然而,這種方法的主要的缺點:是一個程式上升到一個單一的功能。因此,除玩具的例子,它會產生一個巨大的功能,C編譯器是無法在合理的時間內處理。如果這樣設定,應對模組化是不容易的。它要求每個謂詞調用一個諮詢動態表,以便通過本模組,控制開關功能。此外,為支援一個完整的Prolog,也在上下文變化情況下時關注正確處理回溯。因此,支援模組化是懲罰,並且一個額外的模組調用將比一個模組內調用花費大得多。
3.1 Janus
Janus實現是基於一個簡單的思想,通過一個C分支翻譯成WAM分支,即一個goto指令。類似的方法,如[11]中描述的在Prolog編譯器中使用。但是出現了問題,因為簡介分支是標準C(ANSI C)不具備的(因此必須類比),也因為goto指令只能處理在同一代碼功能。該解決方案導致一個C程式組成的一個獨特的功能一個開關指令來類比間接GOTO語句。這種方法之後,我們前面的例子將被轉換為:
fct_switch(){ label_switch: switch(PC){ case p: /* p:- q,r */ label_p: push(CP); /* allocate */ CP=p1; /* call(q) */ goto label_q; /* : */ case p1: pop(CP); /* deallocate */ goto label_r; /* execute(r) */ case q: /* q. */ label_q: PC=CP; /* proceed */ goto label_switch; /* : */ . . . }}
這種方法在RISC機器上花費昂貴,因為switch語句的成本約為10個機器指令(包括邊界檢查)。然而,主要的缺點這種方法是一個程式,上升到一個單一的功能。因此,除玩具的例子,它會產生一個巨大的功能,C編譯器是無法在合理的時間處理。在此設定下,應對模組化是不容易的,它要求每個謂詞調用一個諮詢動態表,以便通過本模組,控制開關功能。此外,為支援一個完整的Prolog,也照顧環境的變化的情況下,正確處理回溯。因此,支援模組化是懲罰性的,並且一個額外的模組調用將比一個模組內調用昂貴得多。
3.2 KL1
作為一個C函數的彙編是不現實的,代表C程式WAM代碼切片成幾個功能。每一個Prolog謂詞看起來那麼自然地翻譯成一個C函數。WAM分枝會給上升到函數調用。這樣一個函數在返回之前調用另一個嵌套函數(分支),依此類推;如此,它永遠不會返回之前結束程式。因此,在C控制棧中積累的資料無用,可導致記憶體溢出。解決的辦法是執行一個分支之前從任何一個函數得到返回,並有一個過程監督器使分支得到足夠的延續。這導致了下面我們繼續的程式碼範例:
fct_supervisor(){ while(PC) (*PC)();}void fct_p() /* p:- q,r, */{ push(CP); /* allocate */ CP=fct_p1; /* call(q) */ PC=fct_q; /* : */}void fct_p1{ pop(CP); /* deallocate */ PC=fct_r; /* execute(r) */}void fct_q() /* q. */{ PC=CP; /* proceed */}
描繪上面的代碼可以抑制PC寄存器最佳化,可以返回其資訊的功能。因此,當傳遞控制和需要分支時,每個功能實現行計算和終端地址返回。這種方法的分析表明一個WAM分支在一個函數調用後,由一個返回實施到監管者。這花費顯然明顯高於簡單jump指令而將產生一個本地代碼編譯器。然而,額外的模組調用現在可能無需支付額外費用。首次實施的wamcc使用這種技術,並且比模擬Sicstus慢兩倍左右。KL1為了減少函數調用和返回,進行了權衡:同一模組內德所以謂詞被翻譯成一個單一的功能。因此,當只有一個模組,KL1行為像Janus。監管功能只需要對額外模組調用環境切換,因此成本超過內部模組調用。
最後,讓我們陳述這種方法(無論是否改善KL1建議)是最適合為100%的ANSI C的解決方案。
3.3 Erlang
在Erlang也被翻譯成一個C函數謂詞。然而,為避免函數調用和返回的開銷,Erlang的優勢是GNU C編譯器(GCC)提供的新的可能性。事實上,GCC認為(jump指令)標籤作為第一類對象,並使得它可以儲存在一個指標變數的標籤並在隨後的執行值的間接跳轉指出這樣的變數。因此,我們的想法是翻譯的WAM分支到簡介跳轉內部C函數的調用,以避免額外的費用。然後需要儲存一個全域表格的所有地址,必須把每個函數的第一次調用初始化。回來說我們不可避免的例子,這將產生:
void fct_p() /* p:- q,r. */{ jmp_tbl[p]=&&label_p; /* (initialization) */ jmp-tbl[p1]=&&labe_p1; return; label_p: push(CP); /* allocate */ CP=&&label_p1; /* call(q) */ goto *jum-tbl[q]; /* : */ label_p1: pop(CP); /* deallocate */ goto *jmp-tbl[r]; /* execute(r) */}void fct_q() /* q. */{ jmp_tbl[q]=&&label_1; /* (initialization) */ return; label_q: goto *CP; /* proceed */}
所有的分支都通過一個全域地址表間接goto。為了消除間接代替直接跳轉的開銷,Erlang像KL1或Janus,一個給定模組的所有謂詞編譯成一個單一功能。因此,只有額外的模組調用需要全域地址表的諮詢,並會就此內部模組調用更加昂貴。
觀察分支直接在函數內部,避免了序言使得它無法使用局部變數(在C堆棧無保留空間)從而意味著只使用局部變數。還要注意的是任何指令必須不移動前項標籤,這是相當難以保證。讓我們考慮到全域表中的元素的訪問。這編譯成一個地址表的負載,其次是用於訪問給定元素的指令表的地址。編譯器可以隨意地最佳化表的訪問,並在函數的一開始放置載入表的地址,它假定它將始終被執行。當在函數內jump時,這將導致一個問題,並會嘗試使用未初始化的寄存器。