Clozure CL中文版018:Clozure CL的实施细节

  • 0

Clozure CL中文版018:Clozure CL的实施细节

Category:帮助手册 Tags : 

Clozure CL的实施细节

本章描述了(大致)1.1版本中OpenMCL实现的许多方面。目前支持的三种体系结构(PPC32,PPC64和x86-64)之间的细节略有不同,这些细节会随着时间的推移而变化,因此最终的参考是源代码(特别是ccl / compiler /目录中包含其名称的一些文件)字符串“arch”和ccl / lisp-kernel /目录中的一些文件,其名称包含字符串“constants”。)希望本章能让那些有兴趣阅读和理解这些文件内容的人更容易。

线程和异常

Clozure CL的线程是“原生的”(意味着它们是由操作系统调度和控制的。)其中的大多数含义在别处讨论; 本节试图从lisp内核的角度描述线程的外观(特别是从GC的角度来看)。

Clozure CL的运行时系统尝试使用机器级异常机制(在可用时为条件陷阱,非法指令,在某些情况下为内存访问保护)来检测和处理异常情况。这些情况包括一些TYPE-ERROR和PROGRAM-ERRORS(特别是错误的args数错误),还包括“无法在没有GCing的情况下分配内存或从操作系统获取更多内存”等情况。一般的想法是,支付(非常偶然)异常处理开销通常会更快,并找出什么’

一些模拟的执行环境(x86版本的Mac OS X上的Rosetta PPC模拟器)不能为异常处理功能提供准确的异常信息。Clozure CL无法在此类环境中运行。

线程上下文记录

首次创建lisp线程时(或者当外部代码创建的线程首次回调到lisp时),将分配并初始化称为线程上下文记录(或TCR)的数据结构。在Linux和FreeBSD的现代版本中,分配实际上是通过一组线程本地存储ABI扩展来实现的,因此线程的TCR在线程创建时创建,在线程死亡时死亡。(世界上最先进的操作系统 – 因为Apple的营销文献提到达尔文 – 在这方面不是很先进,我知道没有理由认为这个领域很快就会取得进展。)

TCR包含几十个字段(因此大小为几百个字节。)这些字段主要是有关线程堆栈位置和大小的特定于线程的信息,有关底层(POSIX)线程的信息以及有关线程的信息。动态绑定历史记录和待处理的CATCH / UNWIND-PROTECT。当线程运行时,其中一些信息可以保存在单独的机器寄存器中(PPC – 有更多可用的寄存器 – 在X86-64必须通过TCR访问的寄存器中保留一些内容),但重要的是请记住,信息是特定于线程的,不能(例如)保存在固定的全局内存位置。

当lisp代码运行时,当前线程的TCR保存在寄存器中。在PPC平台上,使用通用寄存器; 在x86-64上,一个(否则几乎无用的)段寄存器运行良好(为此目的阻止了更普遍有用的通用寄存器的支出。)

TCR的地址在存储器中对齐,使得FIXNUM可用于表示它。lisp函数CCL ::%CURRENT-TCR将调用线程的TCR作为fixnum返回; TCR地址的实际值是此fixnum值的4或8倍。

当lisp内核初始化一个新的TCR时,它被添加到内核维护的全局列表中; 当一个线程退出时,它的TCR将从该列表中删除。

当一个线程调用外部代码时,lisp堆栈指针保存在其TCR中,lisp寄存器(至少那些应该在调用中保留其值的寄存器)保存在线程的值栈中,并且(在x86-64上)RSP切换到控制堆栈。然后设置TCR(tcr.valence)中的字段以指示线程正在运行外部代码,从外部堆栈上的帧加载外部参数寄存器,并调用外部函数。(这有点过于简单,可能不准确,但需要注意的重要事项是线程“停止跟踪lisp堆栈并注册使用惯例”,并且它宣称它已经完成了这一事实。

异常上下文和一般的异常处理

类Unix操作系统倾向于将异常称为“信号”; 相同的通用机制(“信号处理”)用于处理异步OS级事件(例如键盘驱动程序注意到已按下^ C或^ Z的结果)和同步硬件级事件(如尝试执行非法指令或访问受保护的存储器。)推迟(“阻止”)异步信号的处理是有意义的,这样一些关键的代码序列就可以完成而不会中断; 因为在同步异常之后,线程通常不可能继续进行,除非并且直到其状态被异常处理程序修改,

在OSX / Darwin上,POSIX信号处理设施与较低级别的基于Mach的异常处理设施共存。遗憾的是,实现它的方式与调试工具的交互性很差:GDB通常会在目标程序遇到Mach级异常时停止,并且无法从该点开始(并让程序的POSIX信号处理程序尝试处理异常) ); Apple的CrashReporter程序有一个类似的问题,并且根据它的配置方式,可能会使用警告对话框轰炸用户,该对话框错误地声称应用程序已崩溃(实际上有问题的应用程序经常处理例程异常。)在达尔文/ OSX,Clozure CL使用Mach线程级异常处理工具,在GDB或CrashReporter有机会混淆之前运行; Clozure CL的Mach异常处理试图强制接收同步异常的线程调用信号处理函数(“似乎”信号处理在Darwin下更有用。)Mach异常处理程序在专用线程中运行(除了等待基本上什么都不做对于来自lisp内核的异常消息,获取并修改有关发生异常的线程状态的信息,并回复异常消息,并指示已处理异常。来自线程级异常处理程序的回复可以防止将异常报告给GDB或CrashReporter,并避免与这些程序相关的问题。由于Clozure CL的Mach异常处理程序不声称处理与调试相关的异常(来自断点或单步操作),因此可以使用GDB来调试Clozure CL。

在信号处理和调试不相互接触的平台上,输入信号处理程序,阻止所有信号。(此行为在对sigaction()函数的调用中指定,该函数建立了信号处理程序。)信号处理程序从OS内核接收三个参数; 第一个是标识信号的整数,第二个是指向“siginfo_t”类型的对象的指针,它可能包含或不包含一些有助于识别异常原因的字段,第三个参数是指向数据结构的指针(称为“ucontext”或类似的东西),其中包含有关异常/信号发生时线程状态的机器相关信息。当异步信号被阻塞时,信号处理程序将指针存储到当前线程的TCR中的字段中的第三个参数(“信号上下文”),在另一个TCR字段中设置一些位以指示线程现在正在等待处理异常,取消阻塞异步信号,并等待序列化异常处理的全局异常锁定。

在Darwin上,Mach异常线程创建信号上下文(可能是siginfo_t结构),将信号上下文存储在线程的TCR中,设置描述线程状态的TCR字段,并安排线程在其信号处理函数中恢复执行(使用信号处理程序,可能是NULL siginfo_t,信号上下文作为参数。当线程恢复时,它等待全局异常锁定。

在x86-64平台上,信号处理可用于处理同步异常,还有一个额外的复杂因素:OS内核通常在接收信号的线程的堆栈上分配信号上下文和siginfo结构; 在实践中,这意味着“无论RSP指向哪里”。Clozure CL的 注册和堆栈使用惯例 要求线程的值栈 – 在LSP代码运行时RSP通常指向的位置 – 仅包含“节点”(正确标记的lisp对象),并在整个值栈上涂写信号上下文将违反此要求。为了保持一致性,sigaltstack()机制用于使信号在特殊堆栈区域(实际上是线程控制堆栈的最后几页)上传递(并且信号上下文和siginfo被分配)。当信号处理程序运行时,它(小心地)将信号上下文和siginfo复制到线程的控制堆栈,并在调用“真实”信号处理程序之前使RSP指向该堆栈。

一旦异常处理程序获得了全局异常锁定,它就会使用信号编号,siginfo_t和信号上下文参数的值来确定异常的(逻辑)原因。某些异常可能是由应该产生lisp错误或其他严重情况的因素引起的(堆栈溢出); 如果是这种情况,内核代码可能会释放全局异常锁并调用lisp代码。(有问题的lisp代码可能需要重复一些异常解码过程;特别是,它需要能够解释它作为参数接收的信号上下文中的寄存器值。)

在某些情况下,lisp内核异常处理程序可能无法从异常中恢复(这对于某些类型的内存访问错误当前是正确的,对于在外部代码执行期间发生的陷阱或非法指令也是如此。在这种情况下,内核异常处理程序将异常报告为“未处理”,并调用内核调试程序。

如果内核异常处理程序将异常的原因标识为瞬态内存不足情况(指示当前线程需要更多内存),则会尝试使该内存可用。在某些情况下,这样做涉及调用GC。

线程,异常和GC

Clozure CL的GC不是并发的:当响应特定线程中的异常调用GC时,所有其他lisp线程必须停止,直到GC的工作完成。触发GC的线程遍历全局TCR列表,向每个其他线程发送一个独特的“挂起”信号,然后再次遍历列表,等待每个线程信号量,指示线程已收到“挂起”信号并作出适当的回应 一旦所有其他线程都确认了自行挂起的请求,GC线程就可以正常运行GC(在完成任何必要的PC-lusering之后)。)GC完成其工作后,调用GC的线程遍历全局TCR列表,为每个其他线程引发每线程“恢复”信号量。

输入异步“挂起”信号的信号处理程序,阻止所有异步信号。它将其信号上下文参数保存在TCR槽中,提高tcr的“暂停”信号量,然后等待TCR的“恢复”信号量。

当线程收到异常或确认暂停自身的请求时,GC线程可以访问所有TCR(包括它自己的)的信号上下文。此信息(以及有关TCR本身中堆栈区域的信息)允许GC识别作为GC根集的元素的“堆栈位置和寄存器内容”。

PC-lusering

说Clozure CL的编译器和运行时始终遵循精确的堆栈和寄存器使用惯例是不太准确的。有一些例外:

  • 在PPC和x86-64平台上,consing不是完全原子的。它至少需要一些指令才能在内存中分配一个对象(如果需要,可以在其上打一个标题); 如果线程在该指令序列的中间被中断,则新对象可能已经或可能没有在中断发生的时间点被创建或完全初始化。(实际上有一些不同的部分初始化状态)
  • 在PPC上,构建lisp控制堆栈帧的常见行为涉及分配四字帧并将三个寄存器值存储到该帧中。(第四个字 – 前一帧的后向指针 – 在分配帧时自动设置。)这三个字的先前内容是未知的(可能在同一地址有一个外部堆栈帧,前面有几条指令) ,因此中断正在初始化PPC控制堆栈帧的过程中的线程不是GC安全的。
  • 在PPC上初始化临时堆栈帧存在类似的问题。(分配和初始化不会以原子方式发生,并且新分配的堆栈内存可能具有未定义的内容。)
  • 短暂的GC写入屏障必须以原子方式实现(即,代际存储和相应参考位的更新必须不间断地发生,否则这些事件都不会发生。)
  • 还有一些类似的案例。

幸运的是,这些非原子指令序列的数量很少,幸运的是,中断线程很容易识别中断线程何时处于这样的序列中间。当检测到这种情况时,中断线程会修改被中断线程的状态(修改其PC和其他寄存器),使其不再处于这样一个序列的中间(它要么退出它,要么模拟其余的指令)。 )

这是因为(a)许多麻烦的指令序列是PPC特定的,并且相对容易部分地分解围绕PPC上的中断线程PC的指令,以及(b)那些指令序列是高度风格化的并且易于识别。

注册使用和标记

概观

无论其实现的其他细节如何,垃圾收集器的工作是将所有堆分配的lisp对象(CONSes,STRING,INSTANCE等)的集合划分为两个子集。第一个子集包含从一小组“根”对象传递引用的所有对象(GC发生时所有活动线程的堆栈和寄存器的内容以及某些全局变量的值。)第二个子集包含其他一切:那些从根本上无法传递的lisp对象是垃圾,垃圾对象占用的内存可以回收(因为GC刚刚证明它不可能引用它们。)

一组实时可到达的lisp对象基本上形成(通常是大的)图的节点,其中每个节点A的边到对象A引用的任何其他对象(节点)。

此图中的某些节点永远不会具有传出边:具有专用数字或字符类型的数组通常以某种(可能更紧凑)专用方式表示其元素。一些节点可能引用从未在内存中分配的lisp对象(64位平台上的FIXNUM,CHARACTER,SINGLE-FLOAT ……)后一类对象有时被称为“即时”,但这有点令人困惑,因为术语“立即“有时用于指代永远不会成为大连接图的一部分的东西(例如,构成浮点值的”原始“位,外部地址或需要使用的数值 – 至少转瞬即逝 – 在编译的代码中。)

为了使GC能够可靠地构建连接图,它必须能够可靠地告诉(a)“潜在根” – 机器寄存器或堆栈位置的内容 – 实际上是否是节点(b)对于任何节点,它是否可能具有引用其他节点的组件。

没有可靠的方法来回答有关库存硬件的第一个问题。(如果一切都是节点,就像特别是微编码的“lisp机器”硬件那样,它甚至不需要被问到。)因为没有办法只看机器字(机器寄存器的内容)或者堆栈位置)并告诉它是一个节点还是一个随机的非节点值,我们必须采用和强制执行严格的寄存器和堆栈使用约定或容忍歧义。

“容忍歧义”是一些(“保守的”)GC计划采取的方法; 相比之下,Clozure CL的GC是“精确的”,在这种情况下意味着它认为某些机器寄存器和堆栈位置的内容始终是节点,而其他寄存器和堆栈位置永远不是节点,并且这些约定永远不会被违反编译器或运行时系统。线程被抢先调度的事实意味着GC可能在任何指令边界上发生(因为某些其他线程中的活动),这反过来意味着编译器和运行时系统必须始终遵循精确的寄存器和堆栈使用约定

一旦我们确定给定的机器字是一个节点,标记方案就描述了节点的值和类型是如何在该机器字中编码的。

到目前为止,大多数讨论都是从GC的非常低层次的角度来处理问题的。从更高的角度来看,lisp函数接受节点作为参数,将节点作为值返回,并且(通常)对这些参数执行一些操作以产生这些结果。(在许多情况下,所讨论的操作涉及原始非节点值。)lisp类型系统的更高级别部分(如TYPE-OF和CLASS-OF等功能)取决于标记方案

PPC上的pc-locatives

在PPC上,有第三种情况(除了“节点”和“立即”值)。如下所述,表示内存分配的lisp对象的节点是一个偏置(标记)指针到该对象; 通常不可能指向一些复合(多元素)对象(这样的指针不是一个节点,如果要移动底层对象,GC将无法更新指针。)

这样的指针(“进入堆分配对象的内部”)通常称为a 方位; 在Clozure CL中允许定位的情况主要涉及函数调用和返回指令的行为。(为了在技术上准确,另一种情况也出现在x86-64上,但是这种情况不是用户可见的。)

在PowerPC(PPC32和PPC64)上,所有机器指令都是32位宽,所有指令字都是在32位边界上分配的。在PPC Clozure CL中,CODE-VECTOR是一种特殊类型的矢量对象; 其元素是32位PPC机器指令。CODE-VECTOR是FUNCTION对象的属性; 函数调用涉及访问函数的代码向量并跳转到其第一条指令的地址。

当代码矢量中的每个指令顺序执行时,硬件程序计数器(PC)寄存器前进到下一条指令的地址(位于代码矢量中); 由于PPC指令总是32位宽并且在32位边界上对齐,因此PC的低两位始终为0.如果函数执行调用(简单调用指令在PPC上具有助记符“bl”,则表示“分支和链接”),下一条指令的地址(也是一个字对齐的位置到代码矢量中)被复制到专用PPC“链接寄存器”(lr)中; 函数通过“分支到链接寄存器”(blr)指令返回其调用者。

Clozure CL的GC理解某些寄存器包含这些特殊的“pc-locatives”(指向CODE-VECTOR对象的位置); 它包含对查找包含CODE-VECTOR对象的特殊支持,以及如果包含对象在内存中移动则调整所有这些“pc-locatives”。由于架构伪像(固定宽度指令和指令编码的弧形),该操作的第一部分 – 找到包含对象 – 在PPC上是可行和实用的。在x86-64上是不可能的,但幸运的是没有必要(尽管第二部分 – 当包含对象移动时调整PC / RIP)既必要又简单。

注册和堆栈使用惯例

堆栈约定

在PPC和X86平台上,每个lisp线程使用3个堆栈; PPC和X86之间使用这些堆栈的方式不同。

每个帖子都有:

  • 一个“控制堆栈”。在这两个平台上,这是外部代码使用的“堆栈”。在PPC上,它由帧的链表组成,其中每帧中的第一个字指向前一帧中的第一个字(并且最外面的帧指向0).PPC控制栈上的一些帧是lisp帧; lisp帧总是大小为4个字,并且包含(除了指向前一帧的后向指针)调用函数(一个节点),返回地址(调用函数的代码向量中的“locative”)和值应该在函数退出时恢复值堆栈指针(见下文)。在PPC上,GC必须查看控制堆栈帧,识别哪些帧是lisp帧,并将保存的功能槽的内容视为一个节点(并特别处理返回地址)。在x86-64上,控制栈用于直接对象的动态范围分配。由于控制堆栈从不包含x86-64上的节点,因此GC会在该平台上忽略它。控制堆栈的对齐遵循平台的ABI约定(至少在外部代码可以运行的任何时间点。)在PPC上,r1寄存器始终指向当前线程的控制堆栈的顶部; 在x86-64上,RSP寄存器指向当前线程的顶部’ 不运行外部代码时的线程上下文记录。控制堆栈“逐渐减少”。
  • 一个“价值堆栈”。在两个平台上,值堆栈上的所有值都是节点(包括x86-64上的“标记返回地址”。)值堆栈始终与本机字大小对齐; 使用原子指令(pPC上的“stwu”/“stdu”,x86-64上的“push”)总是将对象推送到值堆栈上,因此其底部和顶部之间的值堆栈的内容始终是明确的节点; 编译器通常会在最后一次使用后尽快从值堆栈中弹出或丢弃节点(一旦它们变成垃圾邮件)。在x86-64上,RSP寄存器在运行lisp时寻址值栈的顶部码; 运行外部代码时,该地址保存在TCR中。在PPC上,专用寄存器(VSP,当前为r15)用于在运行lisp代码时寻址值堆栈的顶部,并且在运行外部代码时将VSP值保存在TCR中。价值堆栈增长了。
  • 一个“临时堆栈”。临时堆栈由一个链接的帧列表组成,每个帧都指向前一个临时堆栈帧。每个临时堆栈帧中的本机机器字数始终是偶数,因此临时堆栈在两个字(64位或128位)边界上对齐。临时堆栈用于两个平台上的动态范围对象; 在PPC上,它基本上用于所有这些对象(无论对象是否包含节点); 在x86-64上,立即动态范围对象(字符串,外部指针等)在控制堆栈上分配,并且仅在临时堆栈上分配包含节点的动态范围对象。用于实现CATCH和UNWIND-PROTECT的数据结构存储在ppc和x86-64上的临时堆栈中。临时堆栈帧始终是双节点对齐的,临时堆栈帧内的对象在双节点边界上对齐。每帧中的第一个单词包含一个指向前一帧的后向指针; 在PPC上,第二个字用于向GC指示剩余对象是节点(如果第二个字是0)还是立即(否则)。在x86-64上,临时堆栈帧总是包含节点,第二个字始终为0.临时堆栈逐渐减少。通常需要几条指令来分配和安全地初始化临时堆栈帧’ 旨在包含节点,并且GC必须识别线程正在分配和初始化临时堆栈帧的情况,并注意不要将帧中的任何未初始化的单词解释为节点。当运行lisp代码时,PPC将临时堆栈的当前顶部保存在专用寄存器(TSP,当前为r12)中,并在运行外部代码时将该寄存器的值保存在TCR中。x86-64在线程的TCR中保存每个线程的临时堆栈顶部的地址。并且GC必须识别线程正在分配和初始化临时堆栈帧的情况,并注意不要将帧中的任何未初始化的单词解释为节点。当运行lisp代码时,PPC将临时堆栈的当前顶部保存在专用寄存器(TSP,当前为r12)中,并在运行外部代码时将该寄存器的值保存在TCR中。x86-64在线程的TCR中保存每个线程的临时堆栈顶部的地址。并且GC必须识别线程正在分配和初始化临时堆栈帧的情况,并注意不要将帧中的任何未初始化的单词解释为节点。当运行lisp代码时,PPC将临时堆栈的当前顶部保存在专用寄存器(TSP,当前为r12)中,并在运行外部代码时将该寄存器的值保存在TCR中。x86-64在线程的TCR中保存每个线程的临时堆栈顶部的地址。运行外部代码时TCR中的值。x86-64在线程的TCR中保存每个线程的临时堆栈顶部的地址。运行外部代码时TCR中的值。x86-64在线程的TCR中保存每个线程的临时堆栈顶部的地址。

注册约定

如果有一个“合理的”(对于某些“合理的”值)通用寄存器的数量并且指令集是“合理地”正交的(大多数操作GPR的指令可以在任何GPR上运行),那么它可以静态地将GPR分成至少两组:“立即寄存器”从不包含节点,“节点寄存器”总是包含节点。(在PPC上,一些寄存器是第三组“PC locatives”的成员,并且在两个平台上,一些寄存器可能具有作为堆栈或堆指针的专用角色;

寄存器分区的最终定义是通过“mark_xp()”和“forward_xp()”等函数硬连线到GC中,它将异常帧中某些寄存器的值作为节点处理,并可以对某些特殊处理进行处理。他们在那里遇到的其他注册值。)

在x86-64上,静态寄存器分区方案包括:

  • (仅)三个“立即”寄存器。

RAX,RCX和RDX寄存器用作隐式操作数和一些扩展精度乘法和除法指令的结果,这些指令通常涉及非节点值; 因为它们在这些指令中的使用意味着它们不能保证始终包含节点值,所以将这些寄存器放在“立即”集合中是很自然的。RAX通常被赋予符号名“imm0”,RDX被赋予符号名“imm1”,RCX被赋予符号名“imm2”; 您可以在反汇编代码中看到这些名称,通常是涉及类型检查,数组索引以及外部内存和函数访问的操作。

  • (仅)两个“专用”寄存器。

RSP和RBP具有由硬件和调用约定决定的专用功能。

  • 11个“节点”寄存器。

所有其他寄存器(RBX,RSI,RDI和R8-R15)被声明为包含(几乎)所有时间的节点值; 不使用隐式使用RSI和/或RDI的传统“字符串”操作。

在32位x86上,默认的寄存器分区方案包括:

  • 单个“直接”寄存器。

EAX寄存器的符号名称为“imm0”。

  • 有两个“专用”寄存器。

ESP和EBP具有由硬件和调用约定决定的专用功能。

  • 5个“节点”寄存器。

其余寄存器(EBX,ECX,EDX,ESI,EDI)通常包含节点值。与x86-64一样,不使用隐含使用ESI和EDI的字符串指令。

有时这种默认分区方案不合适。正如x86-64部分所述,有一些指令,如扩展精度MUL和DIV,需要使用EAX和EDX。因此,我们需要一种在运行时更改此分区的方法。

采用了两种方案。第一个使用TCR中的掩码,其中包含每个寄存器的位。如果该位置1,则GC将寄存器解释为节点寄存器; 如果清楚的话,登记册被视为直接登记。第二种方案使用EFLAGS寄存器中的方向标志。如果设置了DF,则EDX被视为立即寄存器。(我们不使用字符串指令,因此不使用DF。)

在PPC上,静态寄存器分区方案包括:

  • 6个“立即”寄存器。

寄存器r3-r8的符号名称为imm0-imm5。作为具有更简单寻址模式的RISC架构,PPC可能比CISC x86-64更频繁地使用立即寄存器,但它们通常用于相同类型的事物(类型检查,数组索引,FFI等)。 )

  • 9个专用寄存器
    • r0(符号名称rzero)在运行lisp代码时始终包含值0。当它用作存储器地址中的基址寄存器时,其值有时读为0; 保持值0有时方便并避免不对称。
    • r1(符号名称sp)是PPC约定的控制堆栈指针。
    • r2用于在ppc64系统上保存当前线程的TCR; 它没有在ppc32上使用。
    • r9和r10(符号名称allocptr和allocbase)用于执行每线程内存分配
    • r11(符号名称nargs)包含条目上的函数参数的数量和多值返回构造中的返回值的数量。由于某些陷阱指令编码的解释方式,它不会更普遍地用作节点或立即寄存器。
    • r12(符号名称tsp)保存当前线程的临时堆栈的顶部。
    • r13用于在PPC32系统上保存TCR; 它没有在PPC64上使用。
    • r14(符号名称loc-pc)用于复制主存储器和函数调用和返回指令中使用的专用PPC寄存器(LR和CTR)之间的“pc-locative”值。
    • r15(符号名称vsp)寻址当前线程的值堆栈的顶部。
    • lr和ctr是函数调用和返回指令中使用的PPC分支单元寄存器; 它们总是被视为“pc-locatives”,这排除了在一些PPC循环结构中使用ctr。
  • 17个“节点”寄存器

r15-r31始终被视为节点寄存器

标记方案

Clozure CL总是在双节点(32位平台为64位,64位平台为128位)边界上分配lisp对象; 这意味着低3位(32位lisp)或4位(64位lisp)始终为0,因此是冗余的(我们只需要知道高位29或60位以识别对齐的对象地址。)lisp节点中的额外位可用于编码至少一些有关节点类型的信息,其他29/60位表示立即值或双节点对齐的存储器地址。节点的低3位或4位称为节点的“标记位”,用于对这些标记位中的类型信息进行编码的约定称为“标记方案”。

可能在所有平台上使用相同的标记方案(至少在具有相同字大小和/或相同数量的可用标记位的所有平台上),但通常有一些强有力的理由不这样做。这些参数往往是特定于机器的:有时候,有一些相当明显的机器相关技巧可以被利用来更快地对某些类型的标记对象进行常见操作; 其他时候,有一些架构限制使得某些类型的某些标签使用起来是不切实际的。(在PPC64上,“ld”(加载双字)和“std”

一种在所有体系结构上运行良好的体系结构依赖标记技巧是对FIXNUM使用0的标记:fixnum基本上编码其值向左移位几位并保持这些低位清晰。FIXNUM加法,减法和二进制逻辑运算可以直接在节点操作数上操作,加法和减法可以利用基于硬件的溢出检测,并且(在没有溢出的情况下)这些操作的硬件结果是节点(fixnum)。其他一些稍微不那么常见的操作可能需要一些额外的指令,但是FIXNUM上的算术运算应该尽可能便宜,并且对FIXNUM使用零标记有助于确保它。

如果我们有N个可用的标记位(对于32位Clozure CL,N = 3,对于64位Clozure CL,N = 4),只要M <= N,这种表示具有强M的低M位的fixnums的方式就可以工作我们制作的M越小,MOST-POSITIVE-FIXNUM和MOST-NEGATIVE的值就越大; 我们制作的N越大,非FIXNUM标签就越明显。合理的折衷方案是选择M = N-1; 这基本上产生两个不同的FIXNUM标签(一个用于偶数fixnums,一个用于奇数fixnums),在32位平台上提供30位fixnums,在64位平台上提供61位fixnums,并为我们留下6或14个标签进行编码其他类型。

一旦我们通过了FIXNUM标签的分配,事情就会迅速转变为机器依赖性。我们可以很容易地看到,我们不能直接标记所有其他原始lisp对象类型,只有6或14个可用标记值; 类型编码的细节在ppc32,ppc64和x86-64实现之间有所不同,但有一些通用的共同原则:

  • CONS单元格总是包含2个元素,并且通常相当常见。因此,为CONS单元格提供自己的标记是有意义的。与fixnum情况不同 – 标签值为0具有正面含义 – 使用任何特定值似乎没有任何优势。(很久以前 – 在68K MCL的情况下 – 选择CONS标签和内存中的CAR和CDR的顺序,以允许更小,更便宜的寻址模式用于“cdr down a list”。这不是ppc的因素或x86-64,但所有版本的Clozure CL仍然将CONS单元的CDR首先存储在内存中。没关系,
  • 无论你如何看待它,NIL有点……不寻常。NIL既是SYMBOL又是LIST(同时也是一个规范的真值,可能是其他一些东西。)它作为LIST的角色对于大多数程序来说可能比它作为SYMBOL的角色更重要:LISTP必须是如果NIL和CAR和CDR之类的原语在安全时隐式执行LISTP并且希望该操作快速运行。有几种可能的方法来解决这个问题; Clozure CL使用其中两个。在PPC32和X86-64上,NIL基本上是一个跨越两个双节点的奇怪的CONS单元; NIL的标签是唯一且一致的模4(64位模8),其中标签用于CONS单元。因此,LISTP适用于低2(或3)位包含适当标记值的任何节点(特殊情况下NIL不需要它。)SYMBOL访问器(SYMBOL-NAME,SYMBOL-VALUE,SYMBOL-PLIST ..) – 必须使用特殊情况NIL(并访问内部代理符号的组件。)在PPC64上(其中体系结构限制规定了可用于访问对象的固定组件的标记集),这种方法不实用。NIL只是一个杰出的SYMBOL,恰好是它的pname槽和值槽与标记指针相同的偏移量,就像CONS单元的CDR和CAR一样。零’ s pname设置为NIL(SYMBOL-NAME检查此字符并返回字符串“NIL”),LISTP(因此安全的CAR和CDR)必须检查(OR NULL CONSP)。至少在CAR和CDR的情况下,PPC具有多个条件代码字段的事实使得额外的测试不会过于昂贵。在IA-32上,我们无法承担为NIL专用标签的费用。因此,NIL只是一个杰出的CONS单元,我们必须在CONSP / RPLACA / RPLACD中明确检查NIL参数。事实上,PPC具有多个条件代码字段,这使得额外的测试不会过于昂贵。在IA-32上,我们无法承担为NIL专用标签的费用。因此,NIL只是一个杰出的CONS单元,我们必须在CONSP / RPLACA / RPLACD中明确检查NIL参数。事实上,PPC具有多个条件代码字段,这使得额外的测试不会过于昂贵。在IA-32上,我们无法承担为NIL专用标签的费用。因此,NIL只是一个杰出的CONS单元,我们必须在CONSP / RPLACA / RPLACD中明确检查NIL参数。
  • 有些对象是立即的(但不是FIXNUM)。对于CHARACTER而言,以及在64位平台上,SINGLE-FLOAT也是如此。运行时系统中使用的某些节点也是如此(例如,用于表示未绑定变量和插槽的特殊值。)在64位平台上,SINGLE-FLOAT具有自己的唯一标记(使它们更容易识别;在在所有平台上,CHARACTER与其他直接对象(未绑定标记)共享一个标记,但很容易识别(通过查看它们的几个低位。)GC将任何具有立即标记的节点(以及具有fixnum标记的任何节点)视为一片树叶。
  • 处理其他所有其他内存分配的对象都不是CONS单元格的一些优点。统一处理也有一些缺点,“内存分配的非CONS对象”的处理并不完全所有Clozure CL实现均匀。让我们首先假装处理是统一的,然后讨论它的方式。“统一方法”是将所有内存分配的非CONS对象视为向量; 这个术语的使用比CL VECTOR类型暗示的要宽松一些。Clozure CL实际上使用术语“uvector”来表示“ 关于x86-64方法的好消息是可以在不引用内存的情况下识别SYMBOL和FUNCTION; 有点坏消息是,对UVECTOR标记的对象起作用的原始操作 – 比如CCL:UVREF函数 – 不能在x86-64上的SYMBOL或FUNCTION上工作(但是对PPC端口中的那些类型的对象起作用)。)存储器中UVECTOR数据之前的标题字在低字节中包含8位类型信息,在该字的其余部分包含24或56位“元素计数”信息。(这是32位平台上ARRAY-TOTAL-SIZE-LIMIT的有时限制值为2 ^ 24的地方。)标题的低字节 – 有时称为uvector的子标签 – 本身被标记(这意味着标记被标记。)子标记中的(3或4)标记位用于确定uvector的元素是节点还是immediates。(其元素为节点的UVECTOR称为GVECTOR;其元素为immediates的UVECTOR称为IVECTOR。此术语来自Spice Lisp,它是CMUCL的前身。)即使标记了uvector标头,标头也不是一个节点。没有(支持的)方式让你在lisp中获得一个并且这样做可能是危险的。

堆分配

当Clozure CL内核首次启动时,进程地址空间的一个大的连续块被映射为“匿名,无访问”内存。(“大”在不同的上下文中表示不同的东西;在LinuxPPC32上,它意味着“大约1千兆字节”,在DarwinPPC32上,它意味着“大约2千兆字节”,在当前的64位平台上,它的范围从128到512千兆字节,具体取决于操作系统。这些值都是默认值和上限; –heap-reserve参数可用于尝试保留少于默认值。)

保留不能(尚未)读取或写入的地址空间不会花费太多; 特别是,它不需要相应的交换空间或物理内存可用。将地址范围标记为“已映射”有助于确保其他内容(来自对malloc(),动态加载的共享库的随机调用的结果)将不会在lisp为其自己的堆增长保留的此区域中分配。

大部分地址空间的一小部分(在32位平台上大约1/32,在64位平台上大约为1/64)保留用于GC数据结构。为这些数据结构保留的内存页面被映射为读写,因为页面可以在堆的主要部分中写入。

初始堆映像映射到此保留地址空间,另外(LISP-HEAP-GC-THRESHOLD)字节映射为读写。GC数据结构增长以匹配初始图像中GC能够存储器的数量加上gc阈值,并且控制转移到lisp代码。不可避免的是,这些代码会破坏一切并开始消耗; 基本上有三层内存分配可以继续。

每线程对象分配

每个lisp线程都有一个私有的“保留内存段”; 当线程启动时,其保留的内存段为空。运行lisp代码时,PPC端口在寄存器中保持当前段中最高的未分配地址和最低可分配地址; 在x86-664上,这些值保留在当前线程的TCR中。(“空”堆段是高指针和低指针相等的段。)当一个线程不在分配内容时,高和低指针的低3位或4位是清晰的(指针是双节点) -aligned。)

线程尝试分配一个对象,其物理大小(以字节为单位)为X,其标记为Y:

  1. 通过( – XY)递减“高”指针
  2. 如果高指针小于低指针则捕获
  3. 如有必要,使用(标记的)高指针初始化对象
  4. 清除高指针的低位

在PPC32上,CONS单元的大小为8字节,CONS单元的标签为1,将arg_z寄存器设置为do(CONS arg_y arg_z)的机器代码如下所示:

(SUBI ALLOCPTR ALLOCPTR 7)    ; decrement the high pointer by (- 8 1)  (TWLLT ALLOCPTR ALLOCBASE)    ; trap if the high pointer is below the base  (STW ARG_Z -1 ALLOCPTR)       ; set the CDR of the tagged high pointer  (STW ARG_Y 3 ALLOCPTR)        ; set the CAR  (MR ARG_Z ALLOCPTR)           ; arg_z is the new CONS cell  (RLWINM ALLOCPTR ALLOCPTR 0 0 28)     ; clear tag bits

在x86-64上,这个想法很相似但实现却不同。指向当前线程保留段的高和低指针保存在TCR中,由Ts段寄存器寻址。x86-64 CONS单元格宽16字节,标签为3; 我们规范地使用temp0寄存器来初始化对象

(subq ($ 13) ((% gs) 216))    ; decrement allocptr  (movq ((% gs) 216) (% temp0)) ; load allocptr into temp0  (cmpq ((% gs) 224) (% temp0)) ; compare to allocabase  (jg L1)                       ; skip trap  (uuo-alloc)                   ; uh, don’t skip trapL1  (andb ($ 240) ((% gs) 216))   ; untag allocptr in the tcr  (movq (% arg_y) (5 (% temp0))) ; set the car  (movq (% arg_z) (-3 (% temp0))); set the cdr  (movq (% temp0) (% arg_z))    ; return the cons

如果我们不采用陷阱(如果分配8-16个字节不会耗尽线程的保留内存段),那么这是一个相当简短的指令序列。如果我们确实采取了陷阱,我们将不得不做一些额外的工作,以获得当前线程的新段。

保留堆段的分配

在首次将lisp映像映射到内存之后 – 并且在每个完整的GC之后 – lisp内核确保(LISP-HEAP-GC-TRESHOLD)超出堆的当前末尾的附加字节被映射为读写。

如果一个线程在尝试分配内存时陷阱,该线程将通过通常的异常处理协议(以确保GCs“看到”捕获线程的状态并序列化异常处理的任何其他线程。)当异常处理程序运行时,它确定失败分配的性质和大小,并尝试代表线程完成分配(并留下一个相当大的特定于线程的内存段,以便下一个小分配不太可能陷阱。

根据所请求的段分配的大小,自上一次GC以来发生的段分配的数量,以及EGC和GC阈值,段分配陷阱处理程序可以在返回新段之前调用完整或短暂的GC。值得注意的是,[E] GC是根据自上次GC以来分配的这些段的数量和大小触发的; 它与每个每个线程段的“完整”程度没有太大关系。大量线程可能会进行相当偶然的内存分配并因此触发GC;

堆积增长

Clozure CL当前运行的所有操作系统默认使用“过度使用”内存分配策略(尽管其中一些提供了覆盖该默认值的方法。)这通常意味着操作系统不一定确保后备存储可用当被要求将页面映射为读写时; 它通常会从映射尝试返回一个成功指示符(将页面映射为“零填充,写时复制”),并且只在非非映射时尝试分配后备存储(交换空间和/或物理内存)零内容被写入页面。

它 – 听起来像是让mmap()调用立即失败更好,但实际上这是一个复杂的问题。(例如,在lisp代码实际触及需要它的页面之前,其他应用程序可能会停止使用某些后备存储。)也不能保证lisp代码能够“干净地”发出内存不足的信号。 lisp是……内存不足

我不知道我曾经见过一个突然的内存不足故障,这个故障之前没有几分钟的过度分页活动。在这种情况下最方便的做法是(a)使用更少的内存或(b)获得更多的内存; 通常很难使用你没有的内存。

GC细节

GC使用Mark / Compact算法; 它的执行时间实际上是堆中实时数据量的一个因素。(稍微知名的Mark / Sweep算法不压缩实时数据,而是遍历垃圾以重建自由列表;因此它们的执行时间是总堆大小的一个因素。)

堆分配中所述,维护了两个辅助数据结构(与lisp堆的大小成比例)。这些是

  1. markbits bitvector,它包含动态堆中每个双节点的位(加上一些额外的字用于对齐,以便子位向量可以从字边界开始。)
  2. 重定位表,其中包含动态堆中每32或64个双节点的本机字,以及用于跟踪堆末​​尾的额外字。

因此,总GC空间开销约为3%(2/64或1/32)。

一般算法如下:

标记阶段

动态堆中的每个双节点在markbits向量中具有相应的位。(对于堆中的任何doublenode,其标记位的索引是通过从对象的地址中减去堆的起始地址并将结果除以8或16来确定的。)GC知道标记位的索引自由指针,因此确定双字地址的markbit索引位于堆的开头和空闲指针之间可以通过单个无符号比较来完成。

在标记阶段开始之前,动态堆中所有双节点的标记位置为零。一个对象是 标如果所有组成双字的标记都设置了,否则没有标记; 设置对象的markbits涉及设置对象中所有组成双节点的相应标记位。

标记阶段遍历每个根。如果root的值的标记指示它是一个非直接节点,其地址位于lisp堆中,则:

  1. 如果对象已标记,则不执行任何操作。
  2. 设置对象的markbit(s)。
  3. 如果对象是ivector,则不做任何进一步操作。
  4. 如果对象是cons单元格,则递归标记其car和cdr。
  5. 否则,该对象是一个gvector。递归标记其元素。

因此,标记对象涉及确保其标记位被设置,然后如果对象最初未被标记,则递归地标记对象内包含的任何指针。如果以明显的方式实现此递归步骤,则标记对象将使堆栈空间与从某个根到该对象的指针链的长度成比例。Clozure CL标记不是将指针链隐式存储在堆栈上(在对mark子例程的一系列递归调用中),而是使用递归和调用的技术的混合链接反转将指针链存储在对象本身中。(递归往往更简单,更快;如果递归步骤注意到堆栈空间变得有限,则使用链接反转技术。)

特殊处理某些类型的对象:

  1. 支持称为的功能 GCTWA (我相信它的首字母缩略词来自MACLISP,它代表“真正无用的原子的垃圾收集”),包含当前包的内部符号的向量在进入标记阶段时被标记,但符号本身不是此时标记。在标记阶段的末尾附近,当且仅当它们以某种方式与新创建的符号区分时(由于它们具有函数绑定,值绑定,plist或其他属性),标记从该向量引用的未以其他方式标记的符号。。)
  2. 在标记任何其他元素之前,池的第一个元素设置为NIL。
  3. 所有哈希表都有某些字段(用于缓存以前的结果)无效。
  4. 弱哈希表和其他弱对象在遇到时会被放在链表中; 只有在存在其他(非弱)引用时才会保留其内容。

在标记阶段结束时,设置从根可传递到的所有对象的标记位,并且所有其他标记位都是清晰的。

搬迁阶段

该 转发地址动态堆中的doublenode是(<其当前地址> – (size_of_doublenode * <其前面未标记的markbits的数量>))或者交替(<堆的基数> +(size_of_doublenode * <标记的markbits的数量)在它之前>))。不是每次都计算先前标记位的数量,而是使用重定位表来预先计算所有双字的转发地址的近似值。给定这个近似地址和指向标记位向量的指针,计算精确的转发地址相对容易。

重定位表包含每个的转发地址 小页,其中pagelet是256字节(或32个doublenodes)。第一个pagelet的转发地址是堆的基础。第二小页面的转发地址是标记位表中第一个32位字中设置的每个标记位的第一个和8个字节的转发地址之和。重定位表中的最后一个条目包含freepointer将具有的转发地址,例如压缩后的freepointer的新值。

在许多程序中,旧对象很少变成垃圾,新对象经常会变成垃圾。构建重定位表时,重定位阶段会记录动态堆中第一个未标记对象的地址。只需要压缩第一个未标记对象和freepointer之间的堆区域; 只需要转发指向该区域的指针(所有其他指向动态堆的指针的转发地址就是该指针的地址。)通常,第一个未标记的对象比自由指针更接近于自由指针的基数。堆。

转发阶段

转发阶段遍历动态堆的所有根和“旧”部分(堆基础和第一个未标记对象之间的部分。)对地址位于第一个未标记对象和空闲指针之间的对象的所有引用都将更新通过使用重定位表和markbits向量和插值,指向对象在压缩后将具有的地址。

找到最靠近对象的pagelet的重定位表条目。如果pagelet的地址小于对象的地址,则在pagelet上的对象之前的set markbits数用于确定对象的地址; 否则,使用在pagelet上跟随对象的设置标记位数。

由于转发视图将堆视为一组双字,因此(大多数)将locative视为任何其他指针。(基本区别在于locative可能看起来被标记为fixnums,在这种情况下,它们被视为对象的字对齐指针。)

如果前向阶段改变了按地址散列的散列表中的任何散列表键的地址(例如,EQ散列表),则它在散列表的头部中设置一个位。如果哈希表代码尝试对这样的表中的键执行查找,则哈希表代码将重新散列哈希表的内容。

分析表明,在GC中花费的总时间的大约一半用在确定指针转发地址的子程序中。利用GCC特定的习惯用法,对例程进行手工编码以及内联调用都可以提高GC的性能。

紧凑的阶段

紧凑阶段压缩第一个未标记对象和自由指针之间的区域,使其仅包含标记对象。在这样做时,它会转发它在复制的对象中找到的任何指针。

当紧凑阶段完成时,GC(或多或少)也是如此:自由指针和一些其他数据结构被更新,控制返回到调用GC的异常处理程序。如果释放了足够的内存来满足可能触发GC的任何分配请求,则异常处理程序返回; 否则,可能在释放一小段紧急记忆池之后发出“记忆严重不足”的情况。

短暂的GC

在Clozure CL内存管理方案中,动态堆中两个对象的相对年龄可以通过它们的地址来确定:如果地址X和Y都是动态堆中的地址,则X小于Y(X的创建时间比Y)如果它比Y更接近自由指针(并且距离堆的底部更远)。

短暂(或世代)垃圾收集器试图利用以下假设:

  • 大多数新创建的对象在创建后很快就会变成垃圾。
  • 已经存活在几个GC中的大多数物体都不可能成为垃圾。
  • 旧对象只能作为破坏性修改的结果指向较新的对象(例如,通过SETF)。

通过集中力量(经常和快速地)回收新创造的垃圾,一个短暂的收集者希望尽可能地推迟更昂贵的全GC。重要的是要注意大多数程序会产生一些长寿命的垃圾,因此EGC通常不能消除对完整GC的需求。

EGC将堆中的每个对象视为仅属于一个对象 代; 世代是按年龄相互关联的一组对象:某些世代是最年轻的,有些是最古老的世代,并且在任何代际世代之间存在年龄关系。通常在首次分配对象时将对象分配给最年轻的一代; 任何在当前一代中存活了一些GC的对象都被提升(或者 终身)进入老一辈。

当生成GCed时,根始终包含堆栈,寄存器和全局变量,以及来自其他代的该生成中对象的任何指针。为了避免需要扫描寻找这种代际引用的那些(通常很大的)其他代,运行时系统必须在它们被创建的位置(通过Setf)记录所有这样的代际引用。(这有时称为“写屏障”:必须注意可能导致代际引用的所有分配,就像其他代被写保护一样)。有时会调用可能包含代际引用的指针集记得的集合。

在Clozure CL的EGC中,堆的组织方式完全相同; “代”只是包含指向堆区域的指针的结构(已经按年龄排序。)当一代需要GCed时,任何年轻一代都被纳入其中; 在给定代的GC中存活的所有对象都被提升到下一代。因此,唯一可以存在的代际引用是那些修改旧对象以包含指向新对象的指针的引用。

EGC使用与完整GC完全相同的代码。当给定的GC是“短暂的”时,

  • 用于确定对象的markbit地址的“堆的基础”是被收集的生成的基础;
  • markbits vector实际上是一个指向全局markbits表中间的指针; 此表中的前面条目用于记录旧代中的双字地址(可能)包含代际引用;
  • 没有执行一些步骤(特别是GCTWA和弱对象的处理);
  • 代际参考表用于查找标记和前进阶段的其他根。如果在代际引用表中设置了一个位,则意味着相应的双字(在某些“旧”代中,在堆的某些“早期”部分中)可能具有指向存储在其中的较年代的对象的指针。

除了一个例外(在进入和退出特殊变量的绑定时发生的隐式setfs),必须记住所有可能引入代际引用的setfs。请注意,初始化对象时发生的隐式setfs(如调用cons或vector的情况)不能引入代际引用,因为新创建的对象总是比用于初始化它的对象更年轻。将任何cons单元或gvector locative推入备忘录堆栈总是安全的; 推送别的东西永远不会安全。

通常,代际引用位向量是稀疏的:存储相对少量的旧位置,尽管它们中的一些可能已经存储了很多次。扫描memoization缓冲区的例程做了很多工作,通常经常这样做; 它使用一种简单的强力方法,但如果更聪明地识别它已经看过的地址,它可能运行得更快。

当EGC标记和正向相位扫描代际参考位时,它们可以清除任何表示绝对不包含代际参考的双字的位。

Fasl文件

保存和加载Fasl文件在xdump / faslenv.lisp,level-0 / nfasload.lisp和lib / nfcomp.lisp中实现。此处的信息仅是概述,在阅读源文件时可能会有所帮助。

Clozure CL Fasl格式是从旧的MCL Fasl格式分叉出来的; 有一些差异,但它们很小。“nfasload”这个名字来自于这个所谓的“新”Fasl系统,这个系统在1986年左右就是如此。

Fasl文件以“文件头”开头,其中包含版本信息和以下“块”的计数。每个Fasl文件通常只有一个“块”。这些块是将多个逻辑文件组合成单个物理文件的机制的一部分,以简化预编译程序的分发。

每个块都以自己的标头开头,它只描述了后面数据的大小。

每个块中的数据被视为一个简单的字节流,它定义了一个字节码程序。实际的字节码“fasl运算符”在xdump / faslenv.lisp中定义。源文件中的描述很简洁,但根据Gary的说法,“可能是准确的”。

一些运算符用于创建每块“对象表”,该对象表是用于跟踪先前加载的对象并简化对它们的引用的向量。创建表时,与其关联的索引设置为零; 这类似于数组填充指针,并允许将表视为堆栈。

每个字节码的低七位用于指定fasl运算符; 目前,约有50个运营商被定义。高字节设置时表示应将操作结果推送到对象表。

大多数字节码后跟操作数; 操作数数据是字节对齐的。有多少个操作数及其类型取决于字节码。操作数可以是对象表的索引,立即值或这些的某种组合。

字节码#xFF是一个例外,它的符号名称为ccl :: $ faslend; 它用于标记块的结尾。

Objective-C桥

Clozure CL如何识别Objective-C对象

在大多数情况下,指向Objective-C类实例的指针被识别为; 识别是(并且可能永远是)略微启发式的。基本上,任何通过基本健全性检查并且其第一个单词是指向已知ObjC类的指针的指针都被认为是该类的实例; Objective-C运行时系统将得出相同的结论。

对于任意内存地址的随机指针当然可能看起来像ObjC实例一样愚弄lisp运行时系统,并且指针可能会使其内容发生变化,从而使某些东西成为真正的ObjC实例(或者看起来很像一个)改变了(可能是因为已被解除分配。)

在第一种情况下,我们可以大大改进启发式:我们可以做出更强的断言,特定指针实际上是“类型:ID”,当它是声明为将这样的指针作为参数或类似声明的函数的函数的参数时结果; 我们可以通过定义为类型的槽的SLOT-VALUE获得更有信心:ID,而不是我们只是在某个地方挖出一个指针。

第二种情况更微妙:ObjC内存管理基于引用计数方案,当lisp仍然引用它时,对象可能……不再是一个对象。如果我们不想处理这种可能性(我们不这样做),我们基本上必须确保对象没有被释放,而lisp仍然认为它是一流的对象。在使用MAKE-INSTANCE创建的对象的情况下,对此有一些支持,但是我们可能需要对以其他方式引入到lisp运行时的外来对象给出类似的处理(作为函数参数,返回值,SLOT-VALUE结果,等等

这还没有完全奏效(实际上,它还没有多少工作); 在实践中,这还没有像预期的那么多问题,但这可能是因为现有的Cocoa代码主要处理相对长寿的对象,如窗口,视图,菜单等。

推荐阅读

可可文档

这是Apple关于Cocoa的所有文档的首页。如果您对Cocoa不熟悉,那么这是一个很好的起点。

Objective-C的基础参考

这是两个最重要的Cocoa引用之一; 它涵盖了除GUI编程之外的所有基础知识。这是一个参考,而不是教程。

http://mip.i3geek.com

Leave a Reply

搜索

分类目录

公 告

本网站学习论坛:

www.zhlisp.com

lisp中文学习源码:

https://github.com/zhlisp/

欢迎大家来到本站,请积极评论发言;

加QQ群学习交流。