Caching 在避免脏工作的同时,对缓存和性能进行批处理操作

Caching 在避免脏工作的同时,对缓存和性能进行批处理操作,caching,haskell,optimization,batch-processing,memoization,Caching,Haskell,Optimization,Batch Processing,Memoization,假设我有两个纯但不安全的函数,它们的作用是相同的,但其中一个正在批量工作,并且渐近更快: f :: Int -> Result -- takes O(1) time f = unsafePerformIO ... g :: [Int] -> [Result] -- takes O(log n) time g = unsafePerformIO ... 天真的实现: getUntil :: Int -> [Result] getUntil 0 = f 0 getUntil n

假设我有两个纯但不安全的函数,它们的作用是相同的,但其中一个正在批量工作,并且渐近更快:

f :: Int -> Result -- takes O(1) time
f = unsafePerformIO ...
g :: [Int] -> [Result] -- takes O(log n) time
g = unsafePerformIO ...
天真的实现:

getUntil :: Int -> [Result]
getUntil 0 = f 0
getUntil n = f n : getUntil n-1
switch
n
值,其中
g
f
便宜


getUntil
实际上将以不断增加的
n
调用,但它可能不会从
0
开始。因此,由于Haskell运行时可以记忆
getUntil
,如果以低于
开关的间隔调用
getUntil
,性能将是最佳的。但是一旦间隔变大,这个实现就会变慢

在命令式程序中,我想我会制作一个树映射(可以快速检查间隙)来缓存所有调用。在缓存未命中时,如果间隔长度大于
switch
,则它将被
g
的结果填充,否则分别被
f
填充

如何在Haskell中对此进行优化

我想我只是在寻找:

  • 使用填充函数按需填充的有序映射,如果缺少的范围小,则使用一个函数填充所有值,直到请求的索引,如果缺少的范围大,则使用另一个函数
  • 映射上的一种get操作,它返回一个列表,其中包含请求索引之前的所有较低值。这将产生一个类似于上面的
    getUntil
    的函数

在我刚刚运行了一些测试之后,我将在我的建议中详细说明如何使用
map

import System.IO
import System.IO.Unsafe
import Control.Concurrent
import Control.Monad

switch :: Int
switch = 1000

f :: Int -> Int
f x = unsafePerformIO $ do
    threadDelay $ 500 * x
    putStrLn $ "Calculated from scratch: f(" ++ show x ++ ")"
    return $ 500*x

g :: Int -> Int
g x = unsafePerformIO $ do
    threadDelay $ x*x `div` 2
    putStrLn $ "Calculated from scratch: g(" ++ show x ++ ")"
    return $ x*x `div` 2

cachedFG :: [Int]
cachedFG = map g [0 .. switch] ++ map f [switch+1 ..]

main :: IO ()
main = forever $ getLine >>= print . (cachedFG !!) . read
…其中,
f
g
开关
具有问题中指出的相同含义

上述程序可以使用GHC按原样编译。执行时,可以输入正整数,然后是换行符,应用程序将根据用户输入的数字打印一些值,再加上一些关于从头开始计算值的额外指示

此程序的简短会话为:

User:     10000 
Program:  Calculated from scratch: f(10000)
Program:  5000000
User:     10001
Program:  Calculated from scratch: f(10001)
Program:  5000500
User:     10000
Program:  5000000
^C
必须手动终止/终止程序

请注意,最后输入的值没有显示“从头开始计算”消息。这表示该程序在某个地方缓存了该值。你可以试着自己执行这个程序;但是要考虑到
threadDelay
的延迟与输入的值成正比

然后可以使用以下方法实现
getUntil
功能:

getUntil :: Int -> [Int]
getUntil n = take n cachedFG
或:


如果您不知道
开关的值,您可以尝试并行计算
f
g
,并使用最快的结果,但这是另一个显示。

fOrG xs=如果长度xs
?或者我还不明白这个问题。“既然Haskell运行时可以记忆
getUntil
。”我并不完全清楚你在问什么,但请注意,Haskell运行时不会记忆函数,例如
getUntil
,除非你专门以一种可以启用记忆的方式编写它。@kosmikus,好吧,好吧,如果GetTill不会被记忆,那么这个版本的优化程度甚至比我想象的还要低。但是,如果我们想象它是被记忆的,它仍然不会有效率,原因如下。@DanielWagner,问题是我不知道如何表述“getUntil”,以便它返回一个关于所有先前n的f或g的列表。这里的函数不知道哪些n被缓存,哪些不被缓存(如果你想象GetTill被内存化了,它似乎不是)。在这种特殊情况下,你不能使用
getTill=map f[0..]
?不需要存储缓存之类的东西。需要时,只需使用
take n getUntil
getUntil :: Int -> [Int]
getUntil = flip take cachedFG