... 就是那个问题。我一直在研究一种算法,该算法将向量数组作为输入,并且该算法的一部分重复选择向量对并评估这两个向量的函数,该函数不会随时间变化。在寻找优化算法的方法时,我认为这将是一个很好的记忆化案例:与其一遍又一遍地重新计算相同的函数值,不如将其缓存并命中缓存。
在跳到代码之前,这里是我的问题的要点:我从记忆中获得的好处取决于向量的数量,我认为这与重复调用的数量成反比,并且在某些情况下记忆会完全降低性能。那么我的情况是否不足以记忆?我是不是做错了什么,有没有更聪明的方法来优化我的情况?
这是一个简化的测试脚本,它非常接近真实的东西:
open System
open System.Diagnostics
open System.Collections.Generic
let size = 10 // observations
let dim = 10 // features per observation
let runs = 10000000 // number of function calls
let rng = new Random()
let clock = new Stopwatch()
let data =
[| for i in 1 .. size ->
[ for j in 1 .. dim -> rng.NextDouble() ] |]
let testPairs = [| for i in 1 .. runs -> rng.Next(size), rng.Next(size) |]
let f v1 v2 = List.fold2 (fun acc x y -> acc + (x-y) * (x-y)) 0.0 v1 v2
printfn "Raw"
clock.Restart()
testPairs |> Array.averageBy (fun (i, j) -> f data.[i] data.[j]) |> printfn "Check: %f"
printfn "Raw: %i" clock.ElapsedMilliseconds
我创建了一个随机向量列表(数据)、一个随机索引集合(testPairs),并在每一对上运行 f。
这是记忆的版本:
let memoized =
let cache = new Dictionary<(int*int),float>(HashIdentity.Structural)
fun key ->
match cache.TryGetValue(key) with
| true, v -> v
| false, _ ->
let v = f data.[fst key] data.[snd key]
cache.Add(key, v)
v
printfn "Memoized"
clock.Restart()
testPairs |> Array.averageBy (fun (i, j) -> memoized (i, j)) |> printfn "Check: %f"
printfn "Memoized: %i" clock.ElapsedMilliseconds
这是我所观察到的: * 当大小很小 (10) 时,记忆的速度大约是原始版本的两倍,* 当大小很大 (1000) 时,记忆比原始版本多 15 倍时间,* 当 f 代价高昂时, memoization 改进了事情
我的解释是,当尺寸较小时,我们有更多的重复计算,并且缓存得到了回报。
令我吃惊的是更大尺寸的巨大性能冲击,我不确定是什么原因造成的。我知道我可以稍微改进字典访问,例如使用结构键 - 但我没想到“天真的”版本表现得如此糟糕。
那么 - 我在做什么明显有问题吗?对于我的情况,记忆是错误的方法吗?如果是,有更好的方法吗?