我很想知道 OCaml 运行时如何处理异常以使它们如此轻量。他们是使用 setjmp/longjmp 还是在每个函数中返回一个特殊值并传播它?
在我看来,longjmp会给系统带来一点压力,但只有在引发异常时,在检查每个函数返回值时需要在调用函数后检查每个值,这似乎我会进行很多检查和跳转,但看起来它的表现最差。
通过查看 OCaml 如何与 C 接口(interface) ( http://caml.inria.fr/pub/docs/manual-ocaml/manual032.html#toc142 ),并查看callback.h,似乎是通过使用对象的内存对齐来标记异常的 ( #define Is_exception_result(v) (((v) & 3) == 2) )。这似乎表明它的实现不使用 longjmp 并在每次函数调用后检查每个函数结果。是这样吗?或者 C 函数已经 try catch 任何异常,然后将其转换为这种格式?
谢谢!
最佳答案
OCaml 异常处理
它不使用 setjmp/longjmp
。当 try <expr> with <handle>
被评估时,一个“陷阱”被放置在堆栈上,其中包含有关处理程序的信息。最顶层陷阱的地址保存在寄存器中,当您引发时,它会直接跳转到该陷阱,一次性展开多个堆栈帧(这比检查每个返回代码更好)。陷阱还存储前一个陷阱的地址,该地址在引发时恢复到寄存器中。
¹:或全局,在寄存器不足的架构上
您可以在代码中亲自查看:
- bytecode compilation :第 635-641 行,两行
Kpushtrap/Kpoptrap
字节码围绕try..with
ed 表达式 - native compilation :第 254-260 行,再次说明
Lpushtrap/Lpoptrap
围绕表达式 - bytecode execution对于字节码
PUSHTRAP
(放置陷阱/处理程序),POPTRAP
(删除它,非错误情况)和RAISE
(跳到陷阱) - native 代码发射 on mips和 on amd64 (例如)
与setjmp
比较
Ocaml 使用非标准调用约定,很少或没有被调用者保存的寄存器,这使得此(和尾递归)高效。我想(但我不是专家)这就是 C longjmp/setjmp
的原因在大多数架构上效率不高。例如,参见this x86_64 setjmp implementation这看起来与之前的捕获机制加上被调用者寄存器保存完全相同。
C/OCaml interface 中已考虑到这一点:从 C 代码调用 Caml 函数的常用方法,caml_callback
,不捕获 OCaml 领域的异常;您必须使用特定的 caml_callback_exn
如果您愿意,它会设置其陷阱处理程序并保存/恢复 C 调用约定的被调用者保存的寄存器。参见例如。 the amd64 code ,保存寄存器然后跳转到 this label设置异常陷阱。
关于exception - OCaml 内部结构 : Exceptions,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8564025/