【问题标题】:Compute as much of a list as possible in a fixed time在固定时间内计算尽可能多的列表
【发布时间】:2012-07-04 10:22:06
【问题描述】:

我想编写一个函数,它需要一个时间限制(以秒为单位)和一个列表,并在时间限制内计算尽可能多的列表元素。

我的第一次尝试是首先编写以下函数,该函数对纯计算进行计时并返回经过的时间以及结果:

import Control.DeepSeq
import System.CPUTime

type Time = Double

timed :: (NFData a) => a -> IO (a, Time)
timed x = do t1 <- getCPUTime
             r  <- return $!! x
             t2 <- getCPUTime
             let diff = fromIntegral (t2 - t1) / 10^12
             return (r, diff)

然后我可以根据这个定义我想要的功能:

timeLimited :: (NFData a) => Time -> [a] -> IO [a]
timeLimited remaining []     = return []
timeLimited remaining (x:xs) = if remaining < 0
    then return []
    else do
        (y,t) <- timed x
        ys    <- timeLimited (remaining - t) xs
        return (y:ys)

但这并不完全正确。即使忽略计时错误和浮点错误,这种方法一旦开始就不会停止列表元素的计算,这意味着它可能(实际上通常会)超出其时间限制。

如果我有一个函数,如果它花费的时间太长,它可能会使评估短路:

timeOut :: Time -> a -> IO (Maybe (a,t))
timeOut = undefined

然后我可以编写我真正想要的函数:

timeLimited' :: Time -> [a] -> IO [a]
timeLimited' remaining []     = return []
timeLimited' remaining (x:xs) = do
    result <- timeOut remaining x
    case result of
        Nothing    -> return []
        Just (y,t) -> do
            ys <- timeLimited' (remaining - t) xs
            return (y:ys)

我的问题是:

  1. timeOut怎么写?
  2. 有没有更好的方法来编写函数timeLimited,例如,不会因多次累加时间差而产生累积的浮点误差?

【问题讨论】:

  • 你不能运行两个线程,其中一个线程倒计时并在达到时间限制后终止计算线程?
  • 也许吧。我没有在 Haskell 中写过很多并发代码。它如何能够返回部分评估的列表?
  • 我可能会将列表放入 TVar 并将每个新节点都添加到其中。刚刚看到 STM.TVar 有一个名为registerDelay 的函数,它也可能有助于同步两个线程。

标签: haskell time timeout


【解决方案1】:

您可以使用timeoutevaluate 提供的类型实现timeOut。它看起来像这样(我省略了计算剩余时间的部分——使用getCurrentTime 或类似的):

timeoutPure :: Int -> a -> IO (Maybe a)
timeoutPure t a = timeout t (evaluate a)

如果你想要比弱头范式更多的强制,你可以用一个已经序列化的参数来调用它,例如timeoutPure (deepseq v) 而不是 timeoutPure v

【讨论】:

  • 这种方法很有用,但超时后不返回部分解。
【解决方案2】:

这是一个我能够使用上面的一些建议来制作的示例。我没有进行大量测试以确保在计时器用完时准确地切断工作,但根据timeout 的文档,这应该适用于所有不使用 FFI 的东西。

import Control.Concurrent.STM
import Control.DeepSeq
import System.Timeout

type Time = Int

-- | Compute as many items of a list in given timeframe (microseconds)
--   This is done by running a function that computes (with `force`)
--   list items and pushed them onto a `TVar [a]`.  When the requested time
--   expires, ghc will terminate the execution of `forceIntoTVar`, and we'll
--   return what has been pushed onto the tvar.
timeLimited :: (NFData a) => Time -> [a] -> IO [a]
timeLimited t xs = do
    v <- newTVarIO []
    _ <- timeout t (forceIntoTVar xs v)
    readTVarIO v 

-- | Force computed values into given tvar
forceIntoTVar :: (NFData a) => [a] -> TVar [a] -> IO [()]
forceIntoTVar xs v = mapM (atomically . modifyTVar v . forceCons) xs

-- | Returns function that does actual computation and cons' to tvar value
forceCons :: (NFData a) => a -> [a] -> [a]
forceCons x = (force x:)

现在让我们在一些昂贵的东西上试一试:

main = do
    xs <- timeLimited 100000 expensiveThing   -- run for 100 milliseconds
    print $ length $ xs  -- how many did we get?

-- | Some high-cost computation
expensiveThing :: [Integer]
expensiveThing = sieve [2..]
  where
      sieve (p:xs) = p : sieve [x|x <- xs, x `mod` p > 0]

使用time 编译和运行,它似乎可以工作(显然在定时部分之外有一些开销,但我大约是 100 毫秒:

$ time ./timeLimited
1234
./timeLimited  0.10s user 0.01s system 97% cpu 0.112 total

另外,关于这种方法的一些注意事项;因为我在一次调用timeout 中封装了运行计算并将它们推送到tvar 的整个操作,所以在创建返回结构时可能会丢失一些时间,尽管我假设(如果你的计算成本很高)它不会占用您的总时间或大部分时间。

更新

现在我已经有一些时间考虑了,由于 Haskell 的懒惰,我不是 100% 肯定上面的注释(关于创建返回结构所花费的时间)是正确的;无论哪种方式,如果这对于您要完成的工作来说不够精确,请告诉我。

【讨论】:

  • 感谢您的回答,它看起来很有希望。似乎有一个障碍 - 如果我(在 GHCi 中)运行它,那么我会得到一些输出列表 x。我可以运行length x 并得到答案,但如果我尝试检查x元素,那么解释器就会挂起。您是否也看到了这种行为?
  • @ChrisTaylor,我没有,但我只是使用我在示例中定义的素数列表。在 ghci 中运行 timeLimited 10 expensiveThing 会产生 [67,61,59,53,47,43,41,37,31,29,23,19,17,13,11,7,5,3,2]。你是在测试这个确切的情况,还是用你的真实计算?他们有什么不同吗?也许尝试更短的时间。
  • @ChrisTaylor 另外,不确定这是否重要,但我正在运行 ghc 7.0.4
  • 我通过计算斐波那契数列来测试它,但我现在已经用你的例子测试了它,同样的事情发生了——如果计算的少于整个列表,那么解释器在我尝试查看结果(使用 ghci 7.4.1)。我现在已经使用deepseq 对其进行了修复,以计算TVar monad 之外的值,这似乎有效——一旦我整理好代码,就会用代码更新我的问题。感谢您的帮助!
  • @ChrisTaylor 所以forceCons 现在看起来更像这样? forceCons x = deepseq x (x:).
【解决方案3】:

当达到超时时,我将使用两个线程和 TVar,并在计算线程中引发异常(导致每个正在进行的事务回滚):

forceIntoTVar :: (NFData a) => [a] -> TVar [a] -> IO [()]
forceIntoTVar xs v = mapM (atomically . modifyTVar v . forceCons) xs

-- | Returns function that does actual computation and cons' to tvar value
forceCons :: (NFData a) => a -> [a] -> [a]
forceCons x = (force x:)

main = do

  v <- newTVarIO []
  tID <- forkIO $ forceIntoTVar args v
  threadDelay 200
  killThread tID
  readTVarIO v 

在此示例中,您(可能)需要稍微调整 forceIntoTVar 以便例如列表节点在原子事务中计算,但首先计算,然后启动原子事务以将它们添加到列表中。

在任何情况下,当引发异常时,正在进行的事务被回滚或正在进行的计算在结果被提供给列表之前停止,这就是你想要的。

您需要考虑的是,当准备节点的单个计算以高频率运行时,与不使用 STM 相比,此示例的成本可能非常高。

【讨论】:

  • 一旦我将forceIntoTVar 修改为使用deepseq,我就可以使用它,以便在事务之外完全计算节点(如您所建议的那样)。感谢您的帮助!
  • @ChrisTaylor 这里使用deepeseq而不是bang模式的原因是什么?
  • 我不知道如何使用爆炸模式:)
  • @ChrisTaylor 我个人更喜欢较小的二进制文件而不是较大的二进制文件。恕我直言,爆炸模式应该产生相同的结果,但会产生更小的二进制文件。
  • 感谢您的提示。您是否认为 bang 模式产生的二进制文件比使用 seq/deepseq 等更小,还是这是事实?我更喜欢代码清晰而不是紧凑的二进制文件,所以我必须查看 bang 模式,看看它们是否能让意图更清晰。
猜你喜欢
  • 2010-09-12
  • 2023-04-02
  • 1970-01-01
  • 1970-01-01
  • 2017-11-06
  • 2019-07-23
  • 1970-01-01
  • 2015-09-17
  • 2019-11-10
相关资源
最近更新 更多