加快runhaskell

时间:2012-02-17 09:53:21

标签: haskell ghc

我有一个小的测试框架。它执行一个循环,执行以下操作:

  1. 生成一个小的Haskell源文件。

  2. 使用runhaskell执行此操作。该程序生成各种磁盘文件。

  3. 处理刚刚生成的磁盘文件。

  4. 这种情况发生了几十次。事实证明,runhaskell占用了程序执行时间的绝大部分。

    一方面,runhaskell设法从磁盘加载文件,标记它,解析它,进行依赖性分析,从磁盘加载20KB以上的文本,标记并解析所有这些,执行完整类型推理,检查类型,desugar到Core,链接到已编译的机器代码,并在解释器中执行这些事情,所有内部都在2秒的时间内,当你想到它时,实际上非常令人印象深刻。另一方面,我仍然希望让它变得更快。 ; - )

    编译测试程序(运行上述循环的程序)产生了微小的性能差异。编译脚本链接的20KB库代码产生了相当明显的改进。但每次调用runhaskell时,它仍然需要大约1秒钟。

    生成的Haskell文件各占1KB,但实际上只有一部分文件发生了变化。也许编译文件并使用GHC的-e切换会更快?

    或者,也许这是重复创建和销毁许多操作系统进程的开销,这会减慢这种速度? runhaskell的每次调用都可能导致操作系统探索系统搜索路径,找到必要的二进制文件,将其加载到内存中(当然这已经在磁盘缓存中?),将其链接到任何DLL,并将其激活起来。有没有什么方法可以(轻松地)保持一个GHC运行实例,而不是不断创建和销毁操作系统进程?

    最终,我想总有GHC API。但正如我所理解的那样,这种噩梦难以使用,高度无证,并且在GHC的每个小点发布时都容易发生根本变化。我试图完成的任务非常简单,所以我真的不想让事情变得更加复杂。

    建议?

    更新:切换到GHC -e(即现在所有编译除了正在执行的一个表达式之外)没有产生可衡量的性能差异。在这一点上似乎很清楚,它是所有操作系统开销。我想知道我是否可以创建一个从测试仪到GHCi的管道,从而只使用一个操作系统进程......

5 个答案:

答案 0 :(得分:9)

好吧,我有一个解决方案:我创建了一个GHCi进程并将其stdin连接到一个管道,以便我可以将它表达式发送给交互式评估。

稍后会有几个相当大的程序重构,整个测试套件现在大约需要8秒才能执行,而不是48秒。这对我有用! :-D

(对于其他任何试图这样做的人:为了爱上帝,记得将-v0开关传递给GHCi,否则你将获得GHCi欢迎旗帜!奇怪的是,如果以交互方式运行GHCi,即使使用-v0命令提示符仍然出现,但是当连接到管道时,命令提示符消失;我认为这是一个有用的设计功能而不是随机事故。)


当然,我走这条奇怪路线的一半原因是我想将stdoutstderr捕获到一个文件中。使用RunHaskell,这很容易;只需在创建子进程时传递适当的选项。但是现在所有测试用例都是由单个操作系统进程运行的,因此没有明显的方法来重定向stdinstdout

我想出的解决方案是将所有测试输出定向到单个文件,并且在测试之间GHCi打印出一个魔术字符串(我希望!)不会出现在测试输出中。然后退出GHCi,啜饮文件,寻找神奇的字符串,这样我就可以把文件剪成合适的块。

答案 1 :(得分:3)

您可能会在TBC中找到一些有用的代码。它有不同的野心 - 特别是废弃测试样板和可能无法完全编译的测试项目 - 但它可以通过监视目录功能进行扩展。测试在GHCi中运行,但是使用cabal成功构建的对象(“runghc Setup build”)。

我开发它来测试具有复杂类型hackery的EDSL,即其他库完成大量计算。

我目前正在将其更新到最新的Haskell平台并欢迎任何评论或补丁。

答案 2 :(得分:2)

如果大多数源文件保持不变,您可以使用GHC的-fobject-code(可能与-outputdir一起)标记来编译一些库文件。

答案 3 :(得分:0)

如果调用runhaskell需要花费很多时间,那么你应该完全消除它吗?

如果您真的需要使用更改Haskell代码,那么您可以尝试以下操作。

  1. 根据需要创建一组具有不同内容的模块。
  2. 每个模块都应该导出它的主要功能
  3. 附加包装模块应根据输入参数从集合中执行正确的模块。每次要执行单个测试时,都会使用不同的参数。
  4. 整个程序是静态编译的
  5. 示例模块:

    module Tester where
    
    import Data.String.Interpolation -- package Interpolation
    
    submodule nameSuffix var1 var2 = [str|
    module Sub$nameSuffix$ where
    
    someFunction x = $var1$ * x
    anotherFunction v | v == $var2$ = v
                      | otherwise = error ("anotherFunction: argument is not " ++ $:var2$)
    
    |]
    
    modules = [ let suf = (show var1 ++ "_" ++ show var2)  in (suf,submodule suf var1 var2) | var1 <- [1..10], var2 <- [1..10]]
    
    writeModules = mapM_ (\ (file,what) -> writeFile file what) modules
    

答案 4 :(得分:0)

如果测试彼此很好地隔离,您可以将所有测试代码放入单个程序中并调用runhaskell一次。如果根据其他测试的结果创建某些测试,或者某些测试调用unsafeCrash,则可能无效。

我假设你生成的代码看起来像这样

module Main where
boilerplate code
main = do_something_for_test_3

您可以将所有测试的代码放入一个文件中。每个测试代码生成器都负责编写do_something_for_test_N

module Main where
boilerplate code

-- Run each test in its own directory
withTestDir d m = do
  cwd <- getCurrentDirectory
  createDirectory d
  setCurrentDirectory d
  m
  setCurrentDirectory cwd

-- ["test1", "test2", ...]
dirNames = map ("test"++) $ map show [1..] 
main = zipWithM withTestDir dirNames tests

-- Put tests here
tests =
  [ do do_something_for_test_1
  , do do_something_for_test_2
  , ...
  ]

现在,您只需要拨打runhaskell一次的费用。