15

所以我只是在学习 Forth 并且很好奇是否有人可以帮助我了解内存管理的一般工作原理。目前我只有(一些)C stack-vs-heap 范式的经验。

据我了解,可以在字典中或堆上进行分配。字典是否像 C 中的堆栈一样更快/更受欢迎?但与 C 不同的是,没有范围和自动堆栈回收,所以我想知道是否有人只将字典用于全局数据结构(如果有的话)。

就堆而言,它很像 C 吗?堆管理是标准 ( ANS ) 概念,还是实现定义的?

4

7 回答 7

15

它不是字典,也不是在堆上——堆的等价物是字典。然而,由于它更像一个堆栈而不是一个堆——新词被添加到字典的末尾(ALLOT通过 FORGET 或 FREE 分配和释放(但释放所有新词——更像多个 POP)) .

实现可以控制内存布局,从而实现传统的堆(或垃圾收集)。一个例子是用于内存管理的堆数据结构的 FORTH 实现(1984)。另一种实现是Quartus Forth (2000) 的动态内存堆。

很多依赖于实现或扩展。例如,内存布局通常是两个块缓冲区(位置由BLOCKTIB),文本输入缓冲区和值以及语言的低级/原始函数,在最低部分,字典在中间(向上增长)和返回堆栈和顶部的参数堆栈1

字典上方第一个可用字节的地址由HERE(它随着字典的扩展而变化)返回。

字典上方还有一个暂存区(PAD返回的地址),用于临时存储数据。暂存区可以看作是空闲内存。

首选的操作模式是尽可能多地使用堆栈而不是局部变量或堆。

1页。286(关于 Forth 的特定版本,MMSFORTH)在“FORTH 的记忆、词典和词汇表”一章中,Forth:文本和参考。Mahlon G. Kelly 和 Nicholas Spies。ISBN 0-13-326349-5 / 0-13-326331-2 (pbk.)。1986 年,普伦蒂斯·霍尔。

于 2012-03-27T22:12:19.663 回答
14

基本问题可能没有以新 Forth 用户需要的方式得到回答,所以我将试一试。

Forth 中的内存可能非常依赖于目标,因此我将把描述限制在最简单的模型上,即平坦的内存空间,代码和数据可以愉快地生活在一起。(相对于分段存储器模型,或用于代码的 FLASH 存储器和用于数据的 RAM 或其他更复杂的模型)

Dictionary 通常从内存的底部开始,由 Forth 系统向上分配。在一个简单的系统中,这两个堆栈将存在于高内存中,并且通常有两个 CPU 寄存器指向它们。(非常依赖系统)

在最基本的层面上,内存是通过改变字典指针变量的值来分配的。(有时称为 DP)

程序员通常不会直接访问这个变量,而是使用一些更高级别的字来控制它。

如前所述,Forth 单词“HERE”返回字典空间中的下一个可用地址。没有提到的是 HERE 是通过获取变量 DP 的值来定义的。(此处为系统依赖性,但对描述有用)

在 Forth 中,'HERE' 可能如下所示:

: 这里 (--addr) DP @ ;

而已。

为了分配一些内存,我们需要将 HERE 向上移动,我们使用“ALLOT”这个词来做到这一点。

'ALLOT' 的 Forth 定义只是从参数堆栈中获取一个数字并将其添加到 DP 中的值。所以无非就是:

: 分配 (n --) DP +! ; \'+!' 将 n 添加到内容变量 DP

FORTH 系统在我们创建新定义时使用 ALLOT,以便我们创建的内容安全地保存在“ALLOTed”内存中。

不太明显的是 ALLOT 可以取负数,因此可以向上或向下移动字典指针。所以你可以分配一些内存并像这样返回它:

十六进制 100 分配

并像这样释放它:

十六进制 -100 分配

综上所述,这是 Forth 系统中最简单的内存管理形式。如何使用它的一个例子可以在单词'BUFFER:'的定义中看到

: 缓冲区: (n --) 创建分配;

'BUFFER:' 在字典中“创建”一个新名称(顺便说一下,create 使用分配为名称腾出空间)然后在名称之后分配 n 个字节的内存以及您的 Forth 系统可能使用的任何相关的内务处理字节

所以现在要分配一个命名内存块,我们只需键入:

MARKER FOO \ 标记内存现在结束的位置

十六进制 2000 缓冲区:IN_BUFFER

现在我们有一个名为 IN_BUFFER 的 8K 字节缓冲区。如果想在标准 Forth 中回收该空间,我们可以键入“FOO”,然后在 FOO 之后在字典中分配的所有内容都将从 Forth 系统中删除。

但是,如果您想要临时内存空间,“这里”上方的所有内容都可以免费使用!

所以你可以简单地指向一个地址并使用它,如果你想喜欢这个

: MYMEMORY 这里 200 + ; \ MYMEMORY 指向 HERE 上方的未分配内存

                        \ MYMEMORY moves with HERE. be aware.

MYMEMORY HEX 1000 ERASE \ 用 2K 字节的零填充它

Forth 通常用于高性能嵌入式应用程序,其中动态内存分配会导致代码不可靠,因此首选使用 ALLOT 进行静态分配。然而,更大的系统有一个堆并使用 ALLOCATE、FREE 和 RESIZE,就像我们在 C 中使用 malloc 等一样。

高炉

于 2016-10-14T18:55:40.527 回答
3

彼得莫滕森很好地阐述了这一点。我将添加一些可能对 C 程序员有所帮助的注释。

堆栈最接近 C 术语“自动”变量以及通常称为局部变量的内容。您可以在某些方面为您的堆栈值命名,但大多数程序员会尝试编写他们的代码,以便命名这些值是不必要的。

从 C 编程的角度来看,字典最好被视为“静态数据”。您可以在字典中保留地址范围,但通常您将使用 ALLOT 和相关词来创建静态数据结构和池,在分配后不会改变大小。如果你想实现一个可以实时增长的链接列表,你可能会为你需要的链接单元分配足够的空间,并编写单词来维护一个可以从中提取的单元的空闲列表。这类东西自然有可用的实现,编写自己的实现是磨练指针管理技能的好方法。

堆分配在许多现代 Forth 中都可用,标准定义了 ALLOCATE、FREE 和 RESIZE 字,它们的工作方式类似于 C 中的 malloc()、free() 和 realloc()。它们返回的内存来自 OS 系统堆,它是通常将地址存储在变量或其他比堆栈更永久的结构中是一个好主意,这样您就不会在释放指针之前无意中丢失指针。附带说明一下,如果发生错误,这些字(连同文件 i/o 字)在堆栈上返回一个非零状态。这个约定非常适合异常处理机制,并允许您编写如下代码:

variable PTR
1024 allocate throw PTR !
\ do some stuff with PTR
PTR @ free throw
0 PTR !

或者对于更复杂的分配/释放示例,如果有些人为的话:

\ A simple 2-cell linked list implementation using allocate and free
: >link ( a -- a ) ;
: >data ( a -- a ) cell + ;
: newcons ( a -- a )    \ make a cons cell that links to the input
   2 cells allocate throw  tuck >link ! ;
: linkcons ( a -- a )   \ make a cons cell that gets linked by the input
   0 newcons dup rot >link ! ;
: makelist ( n -- a )   \ returns the head of a list of the numbers from 0..n
   0 newcons  dup >r
   over 0 ?do
     i over >data ! linkcons ( a -- a )
   loop  >data !  r> ;
: walklist ( a -- )
   begin   dup >data ?  >link @           dup 0= until drop ;
: freelist ( a -- )
   begin   dup >link @  swap free throw   dup 0= until drop ;
: unittest  10 makelist dup walklist freelist ;
于 2012-05-15T21:31:55.627 回答
2

一些 Forth 实现支持返回栈帧和分配内存块的局部变量。例如在SP-Forth中:

lib/ext/locals.f
lib/ext/uppercase.f

100 CONSTANT /buf

: test ( c-addr u -- ) { \ len [ /buf 1 CHARS + ] buf }
  buf SWAP /buf UMIN DUP TO len CMOVE
  buf len UPPERCASE
  0 buf len + C! \ just for illustration
  buf len TYPE
;

S" abc" test \ --> "ABC"
于 2012-03-29T07:48:42.463 回答
2

使用 Forth,您可以进入一个不同的世界。

在 Linux 上的典型 Forth(如 ciforth)中(假设为 64 位),您可以将 Forth 配置为具有与交换空间一样大的线性内存空间(例如 128 GB)。你可以用数组、链表、图片等来填写。您以交互方式执行此操作,通常通过声明变量和包含文件来完成。没有限制。Forth 只为您提供了一个 HERE 指针来帮助您跟踪已用完的内存。即使您可以忽略,1994 年标准中甚至有一个词提供了在空闲内存 (PAD) 中浮动的暂存空间。

有没有类似 malloc() free() 的东西?不必要。在几十 KB 的小内核中,没有。但是您可以只包含一个带有ALLOCATE / FREE的文件,并留出几个 Gbyte 用于动态内存。

例如,我目前正在使用 tiff 文件。一张典型的 140 MB 图片会从字典中提取一小部分内容。像素行被转换,解压缩等。为此我使用动态内存,所以我为一行的解压缩结果分配空间。当结果已用于另一个转换时,我必须再次手动释放它们。感觉和c完全不一样。有更多的控制和更多的危险。

您关于范围等的问题。在 Forth 中,如果您知道地址,则可以访问数据结构。即使您在一张纸上记下了 F7FFA1003。试图通过单独的名称空间使程序更安全在 Forth 风格中并不突出。所谓的单词表(另见词汇表)在这个方向上提供了便利。

于 2018-01-19T16:40:07.333 回答
1

FORTH 内存管理室里藏着一头小象,我没见过太多人提到它。

规范的 FORTH 至少有一个不可寻址的参数堆栈。在我所知道的所有 FORTH 硬件实现中都是这种情况(通常源自 Chuck Moore),它们具有硬件参数堆栈:它没有映射到可寻址的内存空间。

“不可寻址”是什么意思?这意味着:您不能拥有指向参数 stack 的指针,即无法获取该堆栈上事物的地址。堆栈是一个“黑匣子”,您只能通过堆栈 API(如果它是硬件堆栈,则为操作码)访问它,而不能绕过它 - 只有那个 API 会修改它的内容。

这意味着参数堆栈和内存访问之间没有别名使用指针-via@!。这可以轻松高效地生成代码,并且实际上它使得在 FORTH 系统中生成体面的代码比使用 C 和 C++ 更容易获得几个数量级。

当可以获得指向参数堆栈的指针时,这当然会失效。一个设计良好的系统可能会为此类访问提供保护 API,因为在保护范围内,代码生成器必须将所有内容从寄存器溢出到堆栈——也就是说,在没有完整的数据流分析的情况下。

DFA 和其他“昂贵”的优化技术在 FORTH 中当然不是不可能的,只是它们的范围比许多实际的 FORTH 系统要大一些。尽管如此,它们仍然可以非常干净地完成(我在内部 FORTH 实现中使用 CFA、DFA 和 SSA 优化,并且与 LLVM 中的实用程序类相比,整个东西的源代码、包含的注释更少...... - 到处都在使用的类,但实际上并没有做任何与编译或代码分析相关的事情)。

一个实用的 FORTH 系统还可以对返回堆栈内容设置别名限制,即返回地址本身没有别名。这样可以乐观地分析控制流,只考虑通过R@,>R和的显式堆栈访问R>,同时让您在该堆栈上放置可寻址的局部变量 - 这通常在变量大于一个或两个单元格时完成,或者会尴尬保留在参数堆栈上。

在 C 和 C++ 中,自动“局部”变量和指针之间的别名是一个大问题,因为只有具有大型优化器的大型编译器才能证明没有别名并在发生干预指针取消引用时放弃寄存器重新加载/溢出。小型编译器为了保持兼容并且不生成损坏的代码,必须悲观并假设通过char*别名访问所有内容,并通过Type*别名访问该类型和其他“喜欢它”的类型(例如 C++ 中的派生类型)。在char*C 中给所有东西起别名是一个典型的例子,说明你为一个你通常不打算使用的特性付出了高昂的代价。

通常,强制unsigned char字符类型,并使用这种类型重新编写字符串 API,让您不会到处使用char*,并让编译器生成更好的代码。编译器当然会添加大量的分析过程,以尽量减少这种设计失败的后果......在 C 中修复的所有问题是拥有一个byte别名所有其他类型的类型,并且与任意指针兼容,并且具有大小内存的最小可寻址单元。事后看来,重用voidinvoid*表示“指向任何东西”是一个错误,因为返回void意味着什么也不返回,而指向void绝对不意味着“指向任何东西”。

于 2021-07-01T17:06:05.677 回答
0

我的想法发表在https://sites.google.com/a/wisc.edu/memorymanagement 我希望尽快在 github 上发布代码。如果您有一个(或多个)数组,每个数组都有一定数量的特定大小的项目,则可以将一个单一用途的堆栈与每个数组配对。使用每个数组项的地址初始化堆栈。要分配数组项,请从堆栈中弹出一个地址。要解除分配数组项,请将其地址压入堆栈。

于 2021-03-04T20:24:42.827 回答