在C和Haskell的相互递归中编译尾调用优化

时间:2015-11-03 18:19:23

标签: c haskell tail-call-optimization multiple-languages mutual-recursion

我正在试验Haskell中的外部函数接口。我想实现一个简单的测试,看看我是否可以进行相互递归。所以,我创建了以下Haskell代码:

module MutualRecursion where
import Data.Int

foreign import ccall countdownC::Int32->IO ()
foreign export ccall countdownHaskell::Int32->IO()

countdownHaskell::Int32->IO()
countdownHaskell n = print n >> if n > 0 then countdownC (pred n) else return ()

请注意,递归情况是对countdownC的调用,因此这应该是尾递归的。

在我的C代码中,我有

#include <stdio.h>

#include "MutualRecursionHaskell_stub.h"

void countdownC(int count)
{
    printf("%d\n", count);
    if(count > 0)
        return countdownHaskell(count-1);
}

int main(int argc, char* argv[])
{
    hs_init(&argc, &argv);

    countdownHaskell(10000);

    hs_exit();
    return 0;
}

这同样是尾递归。那我就做一个

MutualRecursion: MutualRecursionHaskell_stub
    ghc -O2 -no-hs-main MutualRecursionC.c MutualRecursionHaskell.o -o MutualRecursion
MutualRecursionHaskell_stub:
    ghc -O2 -c MutualRecursionHaskell.hs

并使用make MutualRecursion进行编译。

并且......在运行时,它会在打印8991后出现段错误。 就像确保gcc本身可以在相互递归中处理tco的测试一样,我做了

void countdownC2(int);

void countdownC(int count)
{
    printf("%d\n", count);
    if(count > 0)
        return countdownC2(count-1);
}

void countdownC2(int count)
{
    printf("%d\n", count);
    if(count > 0)
        return countdownC(count-1);
}

这很好用。它也适用于C语言和Haskell中的单递归情况。

所以我的问题是,有没有办法向GHC表明对外部C函数的调用是尾递归的?我假设堆栈帧确实来自从Haskell到C的调用,而不是相反,因为C代码非常明显地是函数调用的返回。

2 个答案:

答案 0 :(得分:3)

我相信跨语言的C-Haskell尾调用非常非常难以实现。

我不知道确切的细节,但C运行时和Haskell运行时非常不同。据我所知,这种差异的主要因素是:

  • 不同的范例:纯粹的功能性与命令性的
  • 垃圾收集与手动内存管理
  • lazy semantics vs strict one

鉴于这种差异,可能在语言边界存活的优化种类几乎为零。理论上,也许可以发明一个特殊的C运行时和Haskell运行时,以便一些优化是可行的,但GHC和GCC并不是这样设计的。

只是为了展示潜在差异的一个例子,假设我们有以下Haskell代码

p :: Int -> Bool
p x = x==42

main = if p 42
       then putStrLn "A"     -- A
       else putStrLn "B"     -- B

main的可能实现可能如下:

  • 在堆栈上推送A的地址
  • 在堆栈上推送B的地址
  • 在堆栈上推送42
  • 跳转到p
  • A:打印“A”,跳到最后
  • B:打印“B”,跳到最后

p的实施方式如下:

  • p:从堆栈中弹出x
  • 从堆栈
  • 中弹出b
  • 从堆栈
  • 中弹出a
  • 测试x反对42
  • 如果相等,请跳至a
  • 跳转到b

注意如何使用两个返回地址调用p,每个可能的结果一个。这与C不同,C的标准实现仅使用一个返回地址。跨越边界时,编译器必须考虑到这种差异并进行补偿。

上面我也没有考虑p的论点是一个thunk的情况,以保持简单。 GHC分配器也可以触发垃圾收集。

请注意,上述虚构实现过去实际上是由GHC(所谓的“推/输”STG机器)使用的。即使现在不再使用它,“eval / apply”STG机器也只是稍微靠近C运行时。我甚至不确定使用常规C堆栈的GHC:我认为它没有,使用它自己的。

您可以查看GHC developer wiki以查看血腥详情。

答案 1 :(得分:0)

虽然我不是Haskel-C interop的专家,但我不认为从C到Haskel的调用可以是一个直接的函数调用 - 它很可能必须通过中介来设置环境。因此,您对haskel的调用实际上包括调用此中间人。这个电话很可能是由gcc优化的。但是从中间人到实际的Haskel例程的调用并没有被完美地优化 - 所以我认为,这就是你正在处理的事情。您可以检查装配输出以确保。