我在Forth和堆栈机上的历史(2010年)

2021-02-21 07:50:00

我的VLSI工具采用了从构思到测试的所有芯片。也许有500行源代码。 Cadence,Mentor Graphics或多或少都这样做。用多少源代码/目标代码?

这是我在实施和使用Forth编程语言以及堆栈计算机体系结构方面的经验的个人说明。 "实施和使用" –按照这个顺序,差不多;这将是显而易见的典型顺序。

这也将变得很清楚,为什么在定义了用于运行Forth并投入生产的处理器的指令集之后,我却不认为自己是合格的Forth程序员(现在是时候警告我对Forth的理解仅仅是那是我自己的理解;不要指望太多。)

为什么关于Chuck Moore的VLSI工具的题词?因为Forth非常激进。黑方有点激进。一种编程方法似乎遗漏了大部分(如果不是全部)编程:

…Forth则有所不同。没有语法,没有冗余,没有打字。没有可以检测到的错误。 ……没有括号。没有缩进。没有钩子,没有兼容性。 …没有文件。没有操作系统。

一般而言,我从来都不是超级至上主义或现代主义的忠实拥护者。但是,如果特定的现代主义者是具有超能力的传统意义上的天才,则很容易引起我的注意。说,他像肖斯塔科维奇一样,在第一眼便能一眼便能记住便条纸。

现在,我已经看到了Cadence和Mentor Graphics之类的芯片设计工具。天文数字昂贵的许可证。地质运行时间。而且没人知道他们在做什么。对我来说,有500条生产线的VLSI工具具有超强能力,足以吸引我的注意力。

自从布鲁斯·埃克尔(Bruce Eckel)撰写的198年代的C ++书中读到它以来,我就对它深感兴趣。他在那儿说,“可扩展性”因诸如Forth之类的语言而声名狼藉,因为程序员可以改变一切并有效地创建自己的编程语言。想!

几年后,我在网上寻找了一些稀缺的信息。一种看起来异常的语言。参数和结果在堆栈上隐式传递。 2 3 +而不是2 + 3。不区分大小写。不过,关于可扩展性业务一无所获。

我认为没有什么比深入了解实现的源头pForth更好的了,我并不需要什么更好的东西,因为我的想法立即被系统顶部的以下段落震惊。在C解释器之上在Forth中实现的pForth的代码:

:(下降41个字;立即 (这是注释词的定义。) (现在,我们可以为正在执行的操作添加注释!)

这样做是定义一个称为"(&#34 ;."(")的单词(函数的名称),该单词在编译时执行(由IMMEDIATE指示)。它告诉编译器从源文件中读取字节(这就是所谓的um和WORD所做的事情),直到找到")– ASCII 41 –。然后忽略(指向它们的指针使用DROP从堆栈中删除)。有效地,"(…)"中的所有内容都变为注释。

哇。是的,您绝对不能在C ++中做到这一点。 (您可以在Lisp中使用,但他们在学校不会教您这些部分。他们教的是纯功能性部分,您无法在C ++中做您可以做的事情。混蛋。)

条件原语 :IF(-f orig)?comp编译0branch conditional_key> mark;即时 :然后(f orig-)swap?condition> resolve;即时 :开始(-f dest)?comp conditional_key< mark;即时 :AGAIN(f dest-)编译分支交换?condition< resolve;即时 :UNTIL(f dest-)编译0branch swap?condition< resolve;即时 :AHEAD(-f orig)编译分支conditional_key> mark;即时

有条件的原语?看起来条件原语不是-它们在这里定义它们。该COMPILE BRANCH业务在编译时修改使用IF或THEN的函数的代码。然后-有条件的一部分-将分支偏移写入(RESOLVE)到条件代码的另一部分IF保存(标记)的代码点。

好像常规程序在编译时修改了从其生成的汇编指令。什么?如何?谁?如何解决这个问题?

有点理解Forth代码是如何表示和解释的。代码是"执行令牌"的此数组。 –基本来说,函数指针,数字和一些内置函数(如分支)。 Forth解释器将指令指针保存在此数组(ip),数据堆栈(ds)和返回堆栈(rs)中,并执行以下操作:

while(true){ 开关(* ip){ //算术(+,-,* ...): 案例加:ds.push(ds.pop()+ ds.pop()); ++ ip; //堆栈操作(drop,swap,rot ...): 案例DROP:ds.pop(); ++ ip; //文字数字(1,2,3 ...): 大小写:ds.push(ip [1]); ip + = 2; //控制流: 情况COND_BRANCH:if(!ds.pop())ip + = ip [1];否则ip + = 2; 案例返回:ip = rs.pop(); //用户定义的字词:保存返回地址&跳 默认值:rs.push(ip + 1); ip = * ip; } }

就是这样。类似于用于实现Java的虚拟堆栈机。一个区别是,编译Forth程序基本上是以WYSIWYG方式写入代码数组。 COMPLEE SOMETHING只是将单词SOMETHING的地址附加到代码数组的末尾。当Forth进行编译而不是解释时,普通的SOMETHING也是如此,因为它位于冒号和分号之间,也就是说,当定义了一个单词时。

只需将{& 2dup,& up,& right,& down,& left,RETURN}附加到代码数组即可。非常简单。没有参数或声明/表达式语法,如…

…要使源代码如何映射到可执行代码还不是很清楚。 " C可以直接映射到程序集&#34 ;?哈! Forth直接映射到装配体。嗯,关于虚拟堆栈机的汇编语言,但是仍然如此。因此,人们可以理解诸如IF和THEN之类的自我修改代码的工作方式。

另一方面,与drawRectangle相比,DRAW-RECTANGLE的功能尚不清楚。 2DUP在有意义的英文名称出现在DRAW-RECTANGLE的定义中之前在堆栈顶部重复的那两个值是什么?应该通过堆栈注释来改善:

…告诉我们DRAW-RECTANGLE希望在堆栈顶部找到高度,而在堆栈下方找到宽度。

我继续了解CREATE / DOES> –您可以使用此编译时自修改代码业务的进一步扩展,以“定义定义单词”。 (例如,常量,变量或类)。 CREATE部分说明了用新的定义词定义词(例如,类名)时应该怎么做。 部分说了使用这些词时应该怎么做。例如:

: 持续的 创建 , 是否> @ ; \用法示例: 连续7天 DAYS-IN-WEEK 2+。 \应该打印9

CREATE意味着每次调用CONSTANT时,都会从源文件中读取一个名称(类似于WORD所做的事情)。然后使用该名称创建一个新单词(就像冒号一样)。该字记录了HERE的值-类似于sbrk(0),它是最后分配的数据项之后的指针。当执行该字时,它将保存的地址压入数据堆栈,然后在DOES>之后调用代码。 CREATE之后的代码可以在此处放置一些数据,以便稍后可用于DOES>部分。

使用CONSTANT,CREATE部分仅保存其输入(在我们的示例中为7)–逗号这样做:* HERE ++ = ds.pop(); 然后,部分将获取保存的数字– @符号是获取词:ds.push(* ds.pop());

CONSTANT的工作方式与类类似,CREATE定义其构造函数,DOES>它的单一方法:

类常量 def initialize(x)@ x = x结束 def dos()@x结束 结尾 daysInWeek = Constant.new(7) 打印daysInWeek.does()+ 2

另一个示例是定义类似C的结构。精简为基本要素(在Forth中,事物往往精简为基本要素),您可以这样说:

…简单地给8(结构尺寸)一个新名称Rect​​angle,并给0和4(成员偏移量)一个新名称,宽度和高度。这是在Forth中实现结构的一种方法:

结构 单元场宽度 细胞场高度 恒定矩形 \用法示例: \这里CREATE仅用于分配 创建r1矩形分配\ r1 = HERE;这里+ = 8 2 r1宽度! 3 r1高度! :区域dup宽度@交换高度@ *; r1区域。 \应该打印6

CELL是一个字的大小;我们可以说" 4字段宽度"而不是"单元格字段宽度"在32b机器上。这是FIELD的定义:

:字段(struct-size field-size-new-struct-size) 创建结束,+ 确实> @ + ;

再次,非常紧凑。 CREATE部分存储偏移量,即当前结构大小(OVER执行ds.push(ds [1]),逗号执行* HERE ++ = ds.pop()),然后将字段大小添加到结构大小中,以将其更新为下次致电FIELD。 部分获取偏移量,然后将其添加到堆栈的顶部(假定包含对象基础指针),以使" rect width"或"身高"分别计算& rect.width或& rect.height。然后,您可以使用@或!访问该地址。 (获取/存储)。 STRUCT只需将0推入数据堆栈的顶部(初始大小值),最后,CONSTANT会使用结构大小:

结构\数据堆栈:0 像元(ds:0 4)场宽(ds:4) 像元(ds:4 4)场高(ds:8) 恒定矩形(ds:和STRUCT一样)

您可以进一步扩展它以支持多态方法-METHOD的工作方式与FIELD相似,通过vtable指针和保留在CREATEd部分中的偏移量来获取函数指针("执行令牌")。因此,可以在一个屏幕中实现Forth中的基本对象系统(Forth代码大小单位– 16行x 64个字符)。

直到今天,令您震惊的是,您可以定义诸如CONSTANT,FIELD,CLASS,METHOD之类的定义词-保留给大多数语言中的内置关键字和语法约定的东西-而且您可以使用如此简单的粗俗工具如此紧凑地完成它实施。当我第一次看到它的时候,我不知道DEFMACRO,以及如何将其用于实现CLOS的定义性单词,例如DEFCLASS和DEFMETHOD(关于Lisp,他们在学校没有教过的另一件事)。因此,Forth完全令人振奋。

它似乎更适合于数字运算/系统编程。而不是文本处理/脚本编写,而正是脚本编写才是将一种语言推入组织的最佳特洛伊木马。脚本通常是任务关键型的,因此无法得到承认,并且许多脚本很小且独立。看看有多少种流行的"脚本语言"与"系统编程语言"相反。然后通过公司支持某种语言使其流行的数量对其进行标准化。显然,脚本是最好的特洛伊木马。

简而言之,在工作中很少有机会和Forth一起玩,所以我没有。我摆弄了解释器和元编程,然后不做任何实际的编程就把它留给了。

以下是与Chuck Moore合作多年的Forth社区杰出成员Jeff Fox对我这样的人的评价:

Forth似乎意味着对某些应用程序进行编程,并将Forth移植或将Forth解剖到其他应用程序。这些团体似乎没有太多共同之处。

…人们通过在自然环境中研究青蛙或通过获得动物学博士学位并专门研究青蛙来学习一套有关青蛙的知识。花一小时在生物学课上解剖一锅甲醛的死青蛙的人会学到关于青蛙的其他知识。

…我最喜欢的例子之一是一位著名的colorforth [Forth方言]爱好者,他花了很多年研究,分解,重新组装和修改它,并对它进行了很多公开评论,但从未打扰过它,并且在两年的学习中一直无法弄清楚如何在colorforth中做某事,例如:

……[Forth用户]对它的功能,使用方式或使用它的人们似乎并不感兴趣。但是有些人花了数年的时间对甚至无法运行的死代码进行尸检。

哎哟。不仅对特定社区的一小部分发动了袭击,而且对一般语言爱好者也发动了袭击。

我想我可以说,如果不能解决现实世界中的重大现实问题,那真的不是Forth。

我猜是对的,从某人广泛使用任何非主流语言并声称专家获得巨大生产力的角度来看,也是如此。对于Forth社区的核心(硬核?)尤其如此,Forth是他们的唯一武器。他们实际上住在福斯。 DIY达到了极致,这在计算历史上可能是空前的,除了Lisp环境和Lisp机器(再一次)。

在Forth芯片上运行的代码。使用Forth CAD工具设计的芯片。在Forth环境中在台式机的裸机上运行的工具。没有标准的操作系统,文件系统或编辑器。在最近几年中,绝对没有其他人会尝试类似的事情。他们声称自己的生产率比C程序员高出10到100倍(非Forth程序员是通用的贬义词; Jeff Fox小心地将" C"用引号引起来,大概是使术语更通用或更多。贬义的)。

......通过仅作为调试器或又一效率的脚本语言来使用1%的时间来使用它的大部分自由,他们淹没了它的大部分自由。

第四是关于改变语言,编译器,操作系统甚至硬件设计的自由,并且与在狭窄的工作环境中拟合事物的编程语言非常不同。

这可以说什么?如果,为了"真的"进入编程文化,我需要兼到"在现实世界中解决一个重要的真正问题。并锻炼"改变语言,编译器,操作系统甚至硬件设计的自由,那么确实有很少的选择进入这种文化。 &#34的要求;现实世界工作"几乎是根据定义与&#34不兼容;改变语言的自由,编译器,操作系统和硬件设计"

然后它发生了,我开始尽可能靠近第四级DIY的现实世界项目。这是我们自己的硬件,我们自己的操作系统我们自己的编译器,旨在运行我们自己的应用程序。我们确实使用标准CAD工具,桌面操作系统和编辑器,并为它们提供标准的RISC核心和标准C ++交叉编译器。好吧,每个人都有弱点。尽管如此,该系统仍然是定制的,嵌入式,优化的独立式,锻炼的自由度 - 非常接近四种方式。

系统的一部分是图像处理协处理器,VLIW主题的变化。它的内存访问和控制流程是奇怪而有限的,既不能访问也不能跳转到任意内存地址。它对我们的图像处理程序的加工密集型部分工作了很好。

我们实际上打算将这些部件与一些&#34粘合在一起;控制指令"设置本机的丰富控制寄存器。当我尝试时,它迅速出现,正如要预期的那样,那些"控制指令"必须能够做到,嗯,一切 - 算术,条件,环。简而言之,我们需要一个CPU。

我们考虑了购买CPU,但目前尚不清楚我们如何使用现成的产品。我们需要从相同的指令流中调度VLIW指令。我们还需要一种奇怪的功能混合。没有缓存,没有中断,不需要超过16个地址位,但要访问64个数据位,以及32位算术。

我们考虑过要制造自己的CPU。全面负责硬件设计的人轻轻地告诉我,我不在意。 CPU具有寄存器文件以及管道和管道停顿以及相关性检测功能,可以避免这些停顿,而且过于复杂。

然后我问,堆栈机怎么样?没有注册文件。只是一个3级流水线-提取,解码,执行。寄存器相关性没有问题,总是从堆栈顶部弹出输入,然后推入结果。

他说听起来很容易,我们可以做到。就像我的RPN计算器一样。您将如何编程?" "在!!"

我在几个小时内定义了指令集。它尽可能直接地映射到Forth单词,此外,Forth还具有C可能不需要的一些东西(作为一种保险)(例如,访问内存中的16位值)。

这已经得到批准和实施;并不是说它成为计划的瓶颈,但这比我们想象的要难。大概部分原因是未阅读" Stack Computers:新的潮流,也不是研究Forth的创造者Chuck Moore的芯片设计。我感觉有见识的人会嘲笑这台机器:将Forth编译成它是微不足道的,但是要付出使硬件复杂化的代价。

但是我很满意–我得到了一个通用CPU,可以通过我的程序在不同时间设置配置注册表,而且副作用是,我得到了Forth目标。即使它不是可以想到的最具成本效益的Forth目标,也绝对是时候开始在工作中使用Forth。

(我无法深入研究的堆栈计算机上的另一个现有技术领域是4stack – 4台真正的VLIW堆栈计算机,顾名思义,它具有4个数据堆栈。我对此非常感兴趣,尤其是在我们担心实现的时候多端口寄存器文件为我们的多个执行单元提供数据的问题我没有弄清楚程序如何映射到4stack,以及当人们不得不将数据从数据栈中溢出到其他内存时效率会下降多少,因为数据流的复杂性。因此,我们只是去了一个标准的寄存器文件,结果就解决了。)

我做的第一件事是为机器编写一个Forth交叉编译器–一个700行的C ++文件(由于未知的原因,这是我见过的最慢的编译C ++代码)。

我忽略了所有元编程内容。例如,上面的所有Forth示例(将我带到Forth的示例)都无法在我自己的Forth中工作。没有字,没有编译,没有立即,没有创建/完成&gt ;,什么也没有。只是冒号定义,RPN语法,内置在编译器中的流控制字。 "优化" –平常不变的折叠,使1 2 +变为3,并内联–:INLINE 1 +;就像:1 +;但内联到调用者的代码中。 (我正在研究瓶颈,因此节省了CALL和RETURN是一件大事。)因此,我有了,再加上VLIW指令的内联汇编。很基本。

我认为我的第一个原型程序不需要更有趣的元编程内容,如果后来发现我错了,可以稍后再添加。抛弃我本来最喜欢的一切都是很奇怪的,但是我准备开始编写真正的程序了。解决现实世界中的现实问题。

除了对库和小型测试程序的各种尝试之外,我最大的程序长约700行(这是1行编译器代码对于1行应用程序代码)。这是一个示例函数:

:mean_std(sum2 sum inv_len-平均值std) \ precision_mean =总和* inv_len; tuck u * \ sum2 inv_len precision_mean \均值= precision_mean>> FRAC; 双重FRAC rshift -ro

......