加速运行haskell

15

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

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

  2. runhaskell来执行。该程序会生成各种磁盘文件。

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

这个过程会重复数十次。结果发现,runhaskell占据了该程序执行时间的绝大部分。

一方面,考虑到runhaskell在2秒钟的墙上时间内能够从磁盘加载文件、对其进行记号化、解析、依赖分析、从磁盘加载20KB的文本、对所有这些内容进行记号和解析、执行完整的类型推断、检查类型、将其降级为核心,并链接到已编译的机器代码中并在解释器中执行,实际上非常令人印象深刻。另一方面,我仍然希望它运行得更快。;-)

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

生成的Haskell文件只有略大于1KB,但实际上只有文件的一部分会发生变化。也许编译文件并使用GHC的-e开关会更快?

或者说,重复创建和销毁许多OS进程的开销正在拖慢速度?每次调用runhaskell可能会导致OS探索系统搜索路径,找到必要的二进制文件,将其加载到内存中(这肯定已经在磁盘缓存中了吧?),与任何DLL链接,并启动它。我是否可以(轻松地)保持一个GHC实例运行,而不是不断地创建和销毁OS进程?

最终,我想总有GHC API这个选择。但据我所知,那个API难以使用,文档极少,而且在每次GHC的小版本发布时都容易发生根本性的改变。我要完成的任务非常简单,所以我不想让事情变得比必要的更加复杂。

有什么建议吗?

更新: 切换到GHC -e(即现在所有代码都被编译,除了要执行的一个表达式),没有明显的性能差异。目前看来很明显是由于操作系统的开销导致的。我在想是否可以从测试程序创建一个管道到GHCi,从而只利用一个操作系统进程...


你的整个工作流程似乎并不是以性能为目标,是吗?为什么你要创建Haskell代码? - leftaroundabout
3
显然你需要一个 GHC 守护进程!:p (有些人曾经开玩笑说要创建一个 grep 守护进程,以避免在启动时不断调用 grep 带来的开销等问题。) - ivanm
1
对于优化的合理尝试和良好执行,加一分。 - user395760
2
不确定其性能如何,但您是否考虑过使用hinthint-server - Dan Burton
ghc -O --make Main && ./program.exe - Thomas Eding
@DanBurton +1,因为你提到了我从未听说过的“hint”。我不认为它能解决这个特定问题(由于需要重定向“stdout”),但它在将来可能会有用。 - MathematicalOrchid
5个回答

9
好的,我有一个解决方案:我创建了一个单独的GHCi进程,并将其stdin连接到一个管道,以便可以发送表达式进行交互式评估。
经过几次相当大的程序重构之后,整个测试套件现在执行起来大约需要8秒钟,而不是48秒钟。对我来说就足够了! :-D
(对于任何试图做到这一点的人:请神爱千万,记得向GHCi传递-v0开关,否则您将会得到一个GHCi欢迎横幅!奇怪的是,即使使用-v0交互运行GHCi,命令提示符仍然出现,但连接到管道时,命令提示符会消失;我假设这是一个有用的设计特性,而不是一个随机事故。)
当然,我走这条奇怪的路线的原因之一是我想将stdout和stderr捕获到一个文件中。使用RunHaskell,这很容易;只需在创建子进程时传递适当的选项即可。但现在所有的测试用例都由一个单独的操作系统进程运行,所以没有明显的方法来重定向stdin和stdout。
我想出的解决办法是将所有测试输出重定向到一个单独的文件中,在测试之间让GHCi打印出一个魔术字符串,这个字符串(希望如此!)不会出现在测试输出中。然后退出GHCi,吞下文件,并查找魔术字符串,以便我可以将文件剪切成适当的块。

你能否更改你的测试函数,使它们接受输出和错误的句柄,而不是直接写入标准输出和标准错误流吗? - Alex

3

TBC中,您可能会发现一些有用的代码。它有不同的目标 - 特别是抓取测试样板和可能无法完全编译的测试项目 - 但它可以通过添加观察目录功能来扩展。测试在GHCi中运行,但使用由cabal成功构建的对象(“runghc Setup build”)。

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

我目前正在将其更新为最新的Haskell平台,并欢迎任何意见或补丁。


2

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


就像我之前说的那样,我已经编译了20KB的库代码。这将运行时间从2秒减少到1秒。但如果有更简单的方法,我想进一步减少运行时间。 - MathematicalOrchid
@MathematicalOrchid 哦,抱歉错过了那一部分 :s - ivanm

0

如果调用runhaskell太耗时间了,那么或许你应该完全消除它?

如果你确实需要处理变化的Haskell代码,那么可以尝试以下方法:

  1. 创建一组具有不同内容的模块。
  2. 每个模块应导出其主函数。
  3. 附加的包装器模块应根据输入参数从一组模块中执行正确的模块。每次您想要执行单个测试时,都需要使用不同的参数。
  4. 整个程序是静态编译的。

示例模块:

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

这样做可能行不通。一些测试程序可能会崩溃;如果整个程序是一个巨大的程序,那么它就会停止运行。此外,我想捕获每个测试的stdoutstderr并将其记录到文件中。如果没有这个要求,那么我可以将整个东西生成为一个单独的Haskell程序。那会容易得多... - MathematicalOrchid
@MathematicalOrchid:你为每个测试重新执行程序,只要一切都编译良好,就不会有问题。关于重定向:./testRunner testNumber123 2>stderr.txt 1>stdout.txt 有什么问题吗? - Tener
“Crash”是什么意思?您应该能够将所有测试集成到一个单独的程序中,并使用顶级测试运行器调用它们,该运行器处理重定向stdoutstderr以及从崩溃中恢复。 - pat

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的开销。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接