我有一个小的测试框架。它执行一个循环,执行以下操作:
生成一个小的Haskell源文件。
使用runhaskell
执行此操作。该程序生成各种磁盘文件。
处理刚刚生成的磁盘文件。
这种情况发生了几十次。事实证明,runhaskell
占用了程序执行时间的绝大部分。
一方面,runhaskell
设法从磁盘加载文件,标记它,解析它,进行依赖性分析,从磁盘加载20KB以上的文本,标记并解析所有这些,执行完整类型推理,检查类型,desugar到Core,链接到已编译的机器代码,并在解释器中执行这些事情,所有内部都在2秒的时间内,当你想到它时,实际上非常令人印象深刻。另一方面,我仍然希望让它变得更快。 ; - )
编译测试程序(运行上述循环的程序)产生了微小的性能差异。编译脚本链接的20KB库代码产生了相当明显的改进。但每次调用runhaskell
时,它仍然需要大约1秒钟。
生成的Haskell文件各占1KB,但实际上只有一部分文件发生了变化。也许编译文件并使用GHC的-e
切换会更快?
或者,也许这是重复创建和销毁许多操作系统进程的开销,这会减慢这种速度? runhaskell
的每次调用都可能导致操作系统探索系统搜索路径,找到必要的二进制文件,将其加载到内存中(当然这已经在磁盘缓存中?),将其链接到任何DLL,并将其激活起来。有没有什么方法可以(轻松地)保持一个GHC运行实例,而不是不断创建和销毁操作系统进程?
最终,我想总有GHC API。但正如我所理解的那样,这种噩梦难以使用,高度无证,并且在GHC的每个小点发布时都容易发生根本变化。我试图完成的任务非常简单,所以我真的不想让事情变得更加复杂。
建议?
更新:切换到GHC -e
(即现在所有编译除了正在执行的一个表达式之外)没有产生可衡量的性能差异。在这一点上似乎很清楚,它是所有操作系统开销。我想知道我是否可以创建一个从测试仪到GHCi的管道,从而只使用一个操作系统进程......
答案 0 :(得分:9)
好吧,我有一个解决方案:我创建了一个GHCi进程并将其stdin
连接到一个管道,以便我可以将它表达式发送给交互式评估。
稍后会有几个相当大的程序重构,整个测试套件现在大约需要8秒才能执行,而不是48秒。这对我有用! :-D
(对于其他任何试图这样做的人:为了爱上帝,记得将-v0
开关传递给GHCi,否则你将获得GHCi欢迎旗帜!奇怪的是,如果以交互方式运行GHCi,即使使用-v0
命令提示符仍然出现,但是当连接到管道时,命令提示符消失;我认为这是一个有用的设计功能而不是随机事故。)
当然,我走这条奇怪路线的一半原因是我想将stdout
和stderr
捕获到一个文件中。使用RunHaskell
,这很容易;只需在创建子进程时传递适当的选项。但是现在所有测试用例都是由单个操作系统进程运行的,因此没有明显的方法来重定向stdin
和stdout
。
我想出的解决方案是将所有测试输出定向到单个文件,并且在测试之间GHCi打印出一个魔术字符串(我希望!)不会出现在测试输出中。然后退出GHCi,啜饮文件,寻找神奇的字符串,这样我就可以把文件剪成合适的块。
答案 1 :(得分:3)
您可能会在TBC中找到一些有用的代码。它有不同的野心 - 特别是废弃测试样板和可能无法完全编译的测试项目 - 但它可以通过监视目录功能进行扩展。测试在GHCi中运行,但是使用cabal成功构建的对象(“runghc Setup build”)。
我开发它来测试具有复杂类型hackery的EDSL,即其他库完成大量计算。
我目前正在将其更新到最新的Haskell平台并欢迎任何评论或补丁。
答案 2 :(得分:2)
如果大多数源文件保持不变,您可以使用GHC的-fobject-code
(可能与-outputdir
一起)标记来编译一些库文件。
答案 3 :(得分:0)
如果调用runhaskell
需要花费很多时间,那么你应该完全消除它吗?
如果您真的需要使用更改Haskell代码,那么您可以尝试以下操作。
示例模块:
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
一次的费用。