为ARM处理器实现Machine Forth
為 ARM 處理器實現 Machine Forth
作者 Reuben Thomas
Computer Laboratory, University of Cambridge
23rd August 1999
摘要
Fox 和 Moore[2] 最近提出了一種新的 Forth 虛擬機模型,稱為 Machine Forth 。使用一個簡單而具體的模型,據說它可以很容易地適應不同的硬件,不需要轉向匯編器就可以產生效率很高的代碼,它也努力成為一個優秀的 Forth 編譯器的基礎。本文討論了一個 ARM 處理器實現 Forth 的方方方面,比較了一個基于 MF 的 Forth 系統和一個使用 ARM 機器模型實現的傳統 Forth 系統。
1 介紹
1999 年 3 月,在 comp.lang.forth 有一個關于 Charles Moore 的 MachineForth 的討論。這個虛擬機模型據說將用于他自己全部的 Forth 編程,并已經在如 F21 一類的幾個硅片上實現了。
Jeff Fox 是 Moore 的助手,他說 Moore 認為 MF VM 對于 Forth 的底層程序員來說比經典的 Forth VM 好得多,它允許離機器更近,同時維護一個相同的虛擬處理機而不管它下面是什么樣的真實處理器。另外,設計的簡單性就意味著為了實現這個系統,所需要的工具不會多于一個小的宏匯編器。
由此而激發興趣,我決定通過在 ARM 處理器上實現一個 Forth 來為我測試這個模型。我想看看像自動增量尋址一類的新指令是如何有利于程序設計的,而缺少 SWAP 和 ROT 一類的指令又是如何妨礙程序設計的,系統的實現有多么簡單,在速度和代碼密度方面與傳統的 Forth 系統有什么差異。當然,我也關心為了適應 ARM 硬件, MF VM 模型需要有哪些改變。
我關于 MF Forth 的測試包括用 MF 寫一個 Forth 編譯器,它含有 Moore 的最小要求。(另一個顯而易見的實驗是把我平時使用的系統 -- 一個擴展的 ANS 兼容系統 -- 移植到 MF 上)。然后,我重新用 ARM 代碼編寫一個編譯器,以比較直接為 ARM 編寫系統和用 MF 編寫之間的差異。在這兩個系統上、在傳統的 Forth 以及 C 編譯器上都運行了基準測試程序。
以下的討論假設讀者熟悉 MF 模型以及 F21 的細節。從這里開始,“Machine Forth ”簡寫為“MF”,而我自己的版本,不好意思,就稱為“MF32”吧。
機器模型的改變
由于我只有 F21 的規范作為 MF 的指導,我不知道 Moore 是否在傳統的處理器上實現了 MF 。
數據寬度
由于 ARM 是32 位的體系結構,數據寬度變成 32 位。
測試進位
在 F21 處理器上,指令 C=0 對堆棧元素的一個擴展位進行測試。如果在傳統的處理器上實現,這個指令將特別慢,所以把這個指令變成測試 T31 位,該位通常用來測試數值是否為負,這對于測試進位沒有幫助,除非我們把寄存器視為 31 位長。不過對于 32 位精度乘法的需要通常比 20 寄存器要少。
堆棧
F21 處理器將它的堆棧放置在芯片上,所以它們是固定大小的;由于 ARM 沒有硬件堆棧,它們可以是任何大小的。
子程序調用和返回
和通常的 RISC 處理器類似, ARM 并不自動地把子程序返回地址放到堆棧上,而是把它轉移到一個寄存器中,這樣就可以避免末端子程序進行存儲器訪問。由于將返回地址壓棧總是很慢,并且每個調用都需要 8 個字節而不是 4 個字節,所以增加了一個新的寄存器 L ( LINK ) , 它像數據棧頂元素寄存器 T 一樣緩沖返回棧頂元素。兩個新的指令是 RET 和 :(冒號),用于實現末端子程序優化。當考慮 L 寄存器的時候,與返回棧相關的指令其語義只需要有很小的變化。
字節尋址
由于 ARM 是字節尋址的,P的增量必須是 4,增加關于字節的讀取和存儲指令看起來是明智的。
乘法
ARM 有硬件乘法指令,所以 +* 可以被 * 代替( +* 除了乘法之外,還有其它的用途,但是,當硬件能夠提供乘法時,刪除一個很少使用的指令看來是合理的)。
NOP
ARM 不需要 no-op 操作,所以刪除了 NOP 指令。這在任何需要的時候都可以通過 PUSH POP 或者 DUP DROP 一類的短語進行模擬。
OS 訪問
增加了一個 SWI 指令以允許 ARM 的 SWI (軟件中斷指令)實現對操作系統的訪問。
2 修訂的 VM 模型
數據元素
棧 : 數據棧 S, 返回棧 R
寄存器 : T 、 L 、 A 、 P
2.2 執行循環
執行在 P 處的指令,如果指令沒有修改 P 則 P = P + 4 ,重復
2.3 指令集
表 1 給出了指令集和它的語義。第一列給出了指令的名字,第二列是立即操作數的數目。它們被稱為 V1 、V2 、…… 第三列給出了語義。使用如下的簡化表示:“PUSH X TO T”意味著 “壓 T 到堆棧 S ,將 X 的值置入 T”, “POP T TO X ”意味著“計算 X ,將 X 的值存入T ,從 S 中彈出 T”。
指令 操作數 Operation
# 1 push V to T
else 1 jump to V
T=0 1 jump to V if T = 0
C=0 1 jump to V if T31 = 0
call 1 set L to P_ 4, jump to V
ret 0 set P to L
: 0 push L to R
; 0 pop P from R
A@ 0 push A to T
A! 0 pop T to A
@A 0 push [A] to T
!A 0 pop T to [A]
B@A 0 push [A]0-7 to T
B!A 0 pop T0-7 to [A]0-7
@A+ 0 push [A] to T, add 4 to A
!A+ 0 pop T to [A], add 4 to A
B@A+ 0 push [A]0-7 to T, add 1 to A
B!A+ 0 pop T0-7 to [A]0-7, add 1 to A
pop 0 push L to T, pop R to L
push 0 push L to R, pop T to L
@R+ 0 push [L] to T, add 4 to L
!R+ 0 pop T to [L], add 4 to L
B@R+ 0 push [L]0-7 to T, add 1 to L
B!R+ 0 pop T0-7 to [L]0-7, add 1 to L
com 0 one's complement T
2* 0 shift T one place left
2/ 0 shift T arithmetically one place right
* 0 set T to [S] = T, pop S
-or 0 set T to exclusive-or of [S] and T, pop S
and 0 set T to and of [S] and T, pop S
+ 0 set T to [S]_ T, pop S
dup 0 push T to T
over 0 push S to T
drop 0 pop T from S
swi 3 pop V2 arguments from T into ARM registers R0 to RV2-1,
call SWI V1, push V3 results from ARM registers R0 to RV3-1 to T
表 1 MF32 指令集
3 匯編器
正如所承諾的那樣,寫匯編器很容易。我增加了常用的匯編控制結構以實現 IF …… THEN 條件和 BEGIN …… REPEAT/AGAIN/UNTIL 循環,增加了MF基于 -IF 的變體。
在完成了編譯器之后,我為匯編器增加了一個簡單的窺孔優化以刪除PUSH-POP 對;這可能比其它的匯編器需要更多的時間才能工作,但是代碼卻變短了。
4 編譯器
編譯器為最小的解釋環境提供了足夠的工具:一個解釋器/編譯器、數值輸入和輸出、具有創建一個新定義的字典能力。
5 Machine Forth 的困難
起步
正如預先看到的那樣,由于不熟悉堆棧和算術操作符,代碼很難編寫,我發現很難記住 A@/A! 和 @A/!A 都是干什么的,我偶爾通過把后者作為 @A+/!A+ 才成功地記住了它們。
隨著時間的推移,我開始發現一些普遍的習慣用法,比如使用 A 和 R 堆棧來改變元素的序列,有時使用 R 來存儲循環計數和終值, DUP BEGIN DROP 去除條件循環的標志(因為 MF 的 IF 和 -IF 指令不彈出 T )。
考慮可移植性
不管從 MF 中得到了什么感覺,我發現在考慮如何最好地實現一個字時,如果不去比較不同的 MF 指令序列與 ARM 翻譯之間的差異是非常困難的。例如,在我的 MF32 實現中,為了在堆棧頂得到常數 0 ,使用 0 常數定義的方法是最快的。在 F21 中,使用 DUP DUP - 可能是最好的。這看起來似乎與 MF 的可移植性相矛盾,盡管在不同的體系結構中使用不同的代碼序列可以得到相同的結果。然而,這個問題在任何的可移植語言中都會出現,不過在 MF 這種簡單的結構中更加明顯罷了。
指令 CACHE 同步
有一個問題是任何的 StrongARM 動態代碼生成器所必須解決的,這就是它的指令 CACHE 不能與數據 CACHE 自動同步,一個簡單和安全的解決方法就是增加一個字 CODE!, 它在地址存入時同步,構造并使用它存入字典(在 MF32 中還有另外一個字就是 THEN )。
增加原語
我發現我需要幾個 MF 沒有提供的功能。我增加了 negate 、 minus 、 or 、 lshift 和 rshift 。我采用的笨拙的規則是:如果一個字用 ARM 指令來寫可以比用 MF32 來寫少幾個指令,我就用 ARM 編碼。
事實上,所有以上提及的都是作為內嵌原語實現的,好像它們是 MF32 匯編器的一部分。
其它用匯編編碼的字
有幾個寫給 comp.lang.forth 的郵件對省略 SWAP 進行了冗長的討論。我沒有參與這種討論,而是在我的系統中增加了 SWAP ,不過它只用了 4 次;其它時候通過一個局部交換,在 A 或者在返回棧中結束,將更加有效。其它的附加原語也很少使用,一般使用 2 - 3 次,唯一的一個例外是 OR ,它使用了 8 次。
EXECUTE 、 MIN 和依賴 OS 的 EMIT 、 SPACE 、 CR 和 BYE 也是用 ARM 匯編寫成的。對于 EXECUTE 需要特別說明 : 在 F21 處理器中它是簡單的 push ret, 但在 MF32 中卻比較困難,盡管可以做。你可以進行一個練習,但在看第 9 部分的答案之前,先思考一下。
缺乏堆棧搗弄
MF 缺乏搗弄堆棧的字,這就給一般的 Forth 程序員增加了壓力,要求他們避免搗弄堆棧,并且更加強烈地要求進行因子分解。初看起來這是一個緊箍咒,但我發現只有 1 、 2 個字由于缺少 PICK 和 ROLL ( NUMBER 特別狡猾)而變得難于編寫。盡管如此,我仍然不習慣于 MF 。
除法
對于數值的輸入和輸出來說,除法是必須的。 MF 和 ARM 都沒有除法指令,所以我使用除法子程序。
讀寫堆棧指針
一個嚴重的缺憾是 MF 不能夠讀寫堆棧指針:它對于在 QUIT 中實現 DEPTH 復位堆棧非常重要。而在 F21 處理器中,由于堆棧是用硬件實現的,所以不太重要,但對于軟件來說,這就非常不同了,因為這可以防止存儲器泄漏。我避免這個問題的方法是:讓 QUIT 簡單地分支到初始化代碼,重新設置堆棧指針。
6 ARM Forth
在結束了 ARM 的 MF32 之后,我用 ARM 代碼編寫了編譯器。這聽起來像 Moore 所謂的“硬件 Forth”, 但有一個不同:當 CMForth 編譯器的硬件結構由一個機器指定時(在這種情況下, 是NOVIX Forth 芯片), ARM 的 Machine Forth 是嚴格地與 Machine Forth 一致的,僅僅是使用 ARM 機器模型而不再是 MF 模型。
代碼不僅更小,至少更容易編寫(實際上,許多字第一次工作,它們不是 MF32 版本的本地碼翻譯,而是完全重寫以便能夠利用 ARM 的優點)。 ARM 小而且規則的指令集在代碼尺寸上完全可以與 Machine Forth (它含有大約 20 條基本指令)相比,但它有更豐富的算術和邏輯操作、尋址模式和 Machine Forth 沒有的特性,比如條件執行。
另一方面,由于 ARM 是基于寄存器的機器,這就導致兩個編譯器:有些字在交互環境中非常有用,比如 DUP 和 + , 但是在編譯代碼中卻很少使用,在編譯代碼中,需要開發寄存器的尋址能力。
7 MF32 和 ARM Forth 的比較
尺寸
表 2 給出了 MF32 和 ARM Forth 系統的一些比較。表中指出代碼密度是每個 MF32 指令為 1.5 ARM 指令或者說是 6 個字節。
表 2: MF32 和 ARM Forth 的比較
窺孔優化節省了大約 100 條指令,相當于全部生成代碼的 9%。
但是我們同時也看到, ARM Forth 比 MF32 多 66 條指令,因為 ARM 芯片的匯編器比 Machine Forth 匯編器更復雜,二進制代碼小了 32 單元,包含 326 個更小的代碼。此外,許多在兩個系統中都有的指令,用 ARM 指令比用 MF32 指令要小。似乎 ARM 的代碼密度是 MF32 的兩倍。
速度
我們把兩個基準程序分別在 MF32 系統、 ARM Forth 系統、一個本地碼子程序串線 ANSI Forth 編譯器( aForth 0.75, 可以從 http://sc3d.org/rrt/ 得到)和 GNU C 上運行。第一個代碼是一個簡單的隨機數發生器,如圖 1 所示(考慮篇幅的原因我們略去了 C 代碼,它是一個 ANS Forth 版本的文字翻譯,可以在 Machine Forth 發布中得到)。
表 3: 隨機數基準測試
表 4 : 素數基準測試
第一次測試運行了 10,000,000 次循環 , 第二次是一個簡單的素數查找器,查找到了 10000 素數(最好是運行一些著名的基準測試,比如 Ertl 的整數測試集,但是沒有足夠的時間把它們翻譯成 Machine Forth 和 ARM Forth )。
ARM Forth 是最小和最快和版本,但是令人驚奇的是 ANSI Forth 代碼也比 MF32 版本更小而且更快(素數基準測試花費了大約 60%的時間用于執行軟件除法子程序,所以這是不重要的,盡管在隨機數基準測試中也表現了同樣的傾向)。
在本文的一個早期版本中,我曾經愚蠢地寫下了“MF32 的速度將明顯地位于子程序串線和本地碼編譯器之間”。盡管通過幾個基準測試就否定這一結論同樣是愚蠢的,但是 MF 說明的速度和代碼密度需要進一步考察,至少在典型的桌面體系結構中應該如此(在 INTEL 的實現中,也發現了類似的代碼密度見 comp.lang.forth )。
圖 1 隨機數基準測試程序
編程的舒適性
ANSI Forth 和 MF32 在編程的舒適性上是相同的。與具有編寫編譯程序的經驗相反, ARM Forth 比較困難。 ARM 代碼也比 Forth 更長更難讀,實際上, ARM Forth 更難因子化,因為在 ARM Forth 中,返回標志的定義將要自然地修改 ARM 寄存器而不是在棧頂返回一個標志值,但這在當前是不可能的,因為標志阻止了跨子程序的調用。也許這一點是需要改變的。
然而,這樣比較 Machine Forth 和 ARM 代碼是不公平的,因為前者是為 MF 而設計的,后者卻不是。使用一個更自然的編碼風格, ARM 匯編將和 Machine Forth 一樣容易寫,并且如果從一個傳統的 Forth 編譯器內部來使用它,它也是可以交互式測試的。
8. 結論
本文只是一個很少經驗的總結,通過寫一個編譯器來得到關于 Forth 編程的結論總是不明智的。很明顯, MF 容易實現,但那只是從機器模型的角度來觀察而得到的結論。對于我來說, Machine Forth 落入了兩難的選擇:對于高級代碼,我更愿意編寫完全標準的 Forth ,對于低級代碼,我更愿意有強大的,高速的、緊縮的匯編器(當然是在一個 Forth 編譯器中使用)。
這個印象也許來自我已經廣泛編程的處理器(6502、MC68000 和 ARM )都有小的和簡單的指令集。對于一個不熟悉的處理器,特別是對于那些大的和復雜的指令集的處理器,與學習本地匯編語言相比, MF 的簡單性也許可以幫助我們更快地產生合理的代碼。
MF 的可移植性也可以作為它的特點被提及,不過那只是編寫高級代碼時的一個優點;當 MF 作為匯編語言的替代時,編碼就無論如何也是機器相關的,這里談論可移植性就沒有任何意義了。
然而, MF 也的確提供了一些有益的思想。對于那些不能提供優化器的小 Forth 編譯器,顯式地使用地址鎖存器就是提高性能的最簡單方法。更有趣的是它非破壞性的條件測試可以很容易地用于傳統的 Forth 中。
所以, MF 的新穎性和傳統簡單性的混合進行仔細研究并混合是有益的,盡管我不會因為這些就放棄了 Forth 和匯編器的傳統組合。
9 練習的答案
EXECUTE 可以這樣地用 MF32 寫出
A! pop A@ push push ;
10 感謝
Jeff Fox was kind enough to read and comment on the paper, a referee made several helpful comments, Hanno Schwalm pointed out the weakness of the primes benchmark, and Marcel Hendrix asked for the comparison with C.
References
[1] M. Anton Ertl. Performance of Forth systems, 1996. http://www.complang.
tuwien.ac.at/forth/performance.html.
[2] Charles Moore and Jeff Fox. Preliminary specification of the F21, 1995. http:
//pisa.rockefeller.edu:8080/MISC/F21.specs.
[3] VLSI Technology Inc., Eaglewood Cliffs, NJ. Acorn RISC Machine family Data Manual,
1990.
總結
以上是生活随笔為你收集整理的为ARM处理器实现Machine Forth的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 缓存面试五连击(下篇)
- 下一篇: 怎么修改照片大小?一键快速修改图片宽高尺